Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert received Int to Bool decoding JSON using Codable

Tags:

swift4

codable

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

like image 764
Hondus Avatar asked Jul 13 '17 20:07

Hondus


People also ask

What does Codable mean in Swift?

Codable is the combined protocol of Swift's Decodable and Encodable protocols. Together they provide standard methods of decoding data for custom types and encoding data to be saved or transferred.

What is the difference between Codable and Decodable in Swift?

Understanding what Swift's Codable isWhen you only want to convert JSON data into a struct, you can conform your object to Decodable . If you only want to transform instances of your struct into Data , you can conform your object to Encodable , and if you want to do both you can conform to Codable .

What's the importance of key decoding strategies when using Codable?

Suggested approach: Give a specific answer first – “key decoding strategies let us handle difference between JSON keys and property names in our Decodable struct” – then provide some kind of practical sample.


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

like image 52
Mojtaba Hosseini Avatar answered Nov 13 '22 10:11

Mojtaba Hosseini


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
}
like image 26
Patrick Avatar answered Nov 13 '22 08:11

Patrick


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
   }
}
like image 14
crashoverride777 Avatar answered Nov 13 '22 08:11

crashoverride777


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.

like image 13
Code Different Avatar answered Nov 13 '22 08:11

Code Different


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"
    }
}
like image 2
OOPer Avatar answered Nov 13 '22 08:11

OOPer