Convert received Int to Bool decoding JSON using Codable
Asked Answered
S

5

15

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."

Someplace answered 13/7, 2017 at 20:51 Comment(3)
Why don't you wrap forKey: .isEnabled within a function that would return a bool true for one 1 and false for 0?Stigmatism
Swift 4.1 fixed this issueSympathetic
A more elegant solution for this problem can be found at https://mcmap.net/q/474101/-swift-4-json-decodable-simplest-way-to-decode-type-changePacifist
N
14

My suggestion: don't fight the JSON. Get it into a Swift value as quickly and with little fuss as possible, then do your manipulation there.

You can define a private internal structure to hold the decoded data, like this:

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

extension JSONModelSettings: Decodable {
    // This struct stays very close to the JSON model, to the point
    // of using snake_case for its properties. Since it's private,
    // outside code cannot access it (and no need to either)
    private struct JSONSettings: Decodable {
        var patient_id: String
        var therapist_id: String
        var enabled: String
    }

    private enum CodingKeys: String, CodingKey {
        case settings
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let settings  = try container.decode(JSONSettings.self, forKey: .settings)
        patientID     = settings.patient_id
        therapistID   = settings.therapist_id
        isEnabled     = settings.enabled == "1"
    }
}

Other JSON mapping frameworks, such as ObjectMapper allows you to attach a transform function to the encoding/decoding process. It looks like Codable has no equivalence for now.

Novelist answered 15/7, 2017 at 9:7 Comment(1)
"don't fight the JSON."Herod
B
15

In those cases I usually like to keep the model like the JSON data, so in your case Ints. Than I add computed properties to the model to convert into Booleans etc

struct Model {
   let enabled: Int
 
   var isEnabled: Bool {
       return enabled == 1
   }
}
Bayard answered 13/4, 2018 at 18:2 Comment(4)
The OP was not able to parse the data at first place.Tramway
Because the OP tried to convert directly to bool which the API does not return.Bayard
Problem here is, you have two properties the developer can access which can lead to errors/confusion. The enabled should be private in this case.Chiaroscuro
True but since this is a domain model I prefer this rather then writing a custom decoder initialiser if it can be avoided and keep the data structure as close as possible to the original API response.Bayard
N
14

My suggestion: don't fight the JSON. Get it into a Swift value as quickly and with little fuss as possible, then do your manipulation there.

You can define a private internal structure to hold the decoded data, like this:

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

extension JSONModelSettings: Decodable {
    // This struct stays very close to the JSON model, to the point
    // of using snake_case for its properties. Since it's private,
    // outside code cannot access it (and no need to either)
    private struct JSONSettings: Decodable {
        var patient_id: String
        var therapist_id: String
        var enabled: String
    }

    private enum CodingKeys: String, CodingKey {
        case settings
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let settings  = try container.decode(JSONSettings.self, forKey: .settings)
        patientID     = settings.patient_id
        therapistID   = settings.therapist_id
        isEnabled     = settings.enabled == "1"
    }
}

Other JSON mapping frameworks, such as ObjectMapper allows you to attach a transform function to the encoding/decoding process. It looks like Codable has no equivalence for now.

Novelist answered 15/7, 2017 at 9:7 Comment(1)
"don't fight the JSON."Herod
L
12

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

Leflore answered 24/6, 2021 at 12:24 Comment(0)
C
5

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
}
Concubine answered 23/6, 2021 at 13:42 Comment(0)
T
2

Decode as a String and then convert it to Bool, just modifying some lines of your code:

("0" is a JSON string, and cannot be decoded as an Int.)

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 = "enabled"//### "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)

        //### decode the value for "enabled" as String
        let enabledString = try user.decode(String.self, forKey: .isEnabled)
        //### You can throw type mismatching error when `enabledString` is neither "0" or "1"
        if enabledString != "0" && enabledString != "1" {
            throw DecodingError.typeMismatch(Bool.self, DecodingError.Context(codingPath: user.codingPath + [SettingsKeys.isEnabled], debugDescription: "value for \"enabled\" needs to be \"0\" or \"1\""))
        }
        //### and convert it to Bool
        isEnabled = enabledString != "0"
    }
}
Teocalli answered 15/7, 2017 at 11:46 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.