Convert received Int to Bool decoding JSON using Codable

Viewed 10972

I have structure like this:

struct JSONModelSettings {
    let patientID : String
    let therapistID : String
    var isEnabled : Bool

    enum CodingKeys: String, CodingKey {
        case settings // The top level "settings" key
    }

    // The keys inside of the "settings" object
    enum SettingsKeys: String, CodingKey {
        case patientID = "patient_id"
        case therapistID = "therapist_id"
        case isEnabled = "is_therapy_forced"
    }
}

extension JSONModelSettings: Decodable {
    init(from decoder: Decoder) throws {

        // Extract the top-level values ("settings")
        let values = try decoder.container(keyedBy: CodingKeys.self)

        // Extract the settings object as a nested container
        let user = try values.nestedContainer(keyedBy: SettingsKeys.self, forKey: .settings)

        // Extract each property from the nested container
        patientID = try user.decode(String.self, forKey: .patientID)
        therapistID = try user.decode(String.self, forKey: .therapistID)
        isEnabled = try user.decode(Bool.self, forKey: .isEnabled)
    }
}

and JSON in this format (structure used to pull keys from setting without extra wrapper):

{
  "settings": {
    "patient_id": "80864898",
    "therapist_id": "78920",
    "enabled": "1"
  }
}

Question is how can i convert "isEnabled" to Bool, (getting 1 or 0 from API) When im trying to parse response im getting error: "Expected to decode Bool but found a number instead."

5 Answers

Property Wrapper

To decode Strings, Ints, Doubles or Bools to a Bool,

just put @SomeKindOfBool before the boolean property like:

@SomeKindOfBool public var someKey: Bool

Demo:

struct MyType: Decodable {
    @SomeKindOfBool public var someKey: Bool
}

let jsonData = """
[
 { "someKey": "true" },
 { "someKey": "yes" },
 { "someKey": "1" },

 { "someKey": 1 },

 { "someKey": "false" },
 { "someKey": "no" },
 { "someKey": "0" },

 { "someKey": 0 }
]
""".data(using: .utf8)!

let decodedJSON = try! JSONDecoder().decode([MyType].self, from: jsonData)

for decodedType in decodedJSON {
    print(decodedType.someKey)
}

The powerful PropertyWrapper implementation behind this:

@propertyWrapper
struct SomeKindOfBool: Decodable {
    var wrappedValue: Bool

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        //Handle String value
        if let stringValue = try? container.decode(String.self) {
            switch stringValue.lowercased() {
            case "false", "no", "0": wrappedValue = false
            case "true", "yes", "1": wrappedValue = true
            default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expect true/false, yes/no or 0/1 but`\(stringValue)` instead")
            }
        }

        //Handle Int value
        else if let intValue = try? container.decode(Int.self) {
            switch intValue {
            case 0: wrappedValue = false
            case 1: wrappedValue = true
            default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expect `0` or `1` but found `\(intValue)` instead")
            }
        }

        //Handle Int value
        else if let doubleValue = try? container.decode(Double.self) {
            switch doubleValue {
            case 0: wrappedValue = false
            case 1: wrappedValue = true
            default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expect `0` or `1` but found `\(doubleValue)` instead")
            }
        }

        else {
            wrappedValue = try container.decode(Bool.self)
        }
    }
}

If you need to implement an optional one, check out this answer here

It's 2021 and we have simpler ways of solving this in Swift 5 using PropertyWrappers.

@propertyWrapper
struct BoolFromInt: Decodable {
    var wrappedValue: Bool // or use `let` to make it immutable
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let intValue = try container.decode(Int.self)
        switch intValue {
        case 0: wrappedValue = false
        case 1: wrappedValue = true
        default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expected `0` or `1` but received `\(intValue)`")
        }
    }
}

Usage:

struct Settings: Decodable {
    @BoolFromInt var isEnabled: Bool
}
Related