Search code examples
swiftjsondecoder

Custom intialiser on Primitive types for JSONDecoder


How do I customise the behaviour of JSONDecoder for primitive types like Int, Bool?

Here is the problem:

  • Backend cannot be relied upon for types. Eg: Bool can come as true/false or "true"/"false"(bool can come wrapped in double quotes)

  • We have at least 300 Codable structs having average 15 properties in them and writing decoding logic for all of them is cumbersome. Also the logic remains more or less same hence the code has become repetitive

Hence, I am looking for a solution such that if there is a Type mismatch then primitive types should be able to handle it and if not then it should be set to nil if that type is Optional.


I tried multiple approaches for this

1. Having Wrapper on all the primitive types and handling the decoding logic. Below is an example of wrapper on Bool

struct SafeBool: Codable {
    private var bool: Bool?

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let b = try? container.decode(Bool.self) {
            self.bool = b
        } else if let string = try? container.decode(String.self) {
            if string.lowercased() == "true" {
                self.bool = true
            } else if string.lowercased() == "false" {
                self.bool = false
            } else {
                throw Error()
            }
        }
    }
}

This, although solves the problem, but creats unnecessary confusion among fellow developers as Wrapped types do not come out as naturally as the Native ones. Also the value cannot be accessed directly (it always need xyz.bool) to extract the raw value

2. Create a new protocol inherited from Decodable and subclass JSONDecoder

protocol KKDecodable: Decodable {
    init(decoder1: Decoder)
}

extension Bool: KKDecodable {
    init(decoder1: Decoder) {
     // Logic for creating Bool from different types
    }
}

class JSONDecoder1: JSONDecoder {
    func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : KKDecodable {
         // Some code that would invoke `init(decoder1: Decoder)`
         // which is defined in `KKDecodable`
    }
}

I was not able to write working code using this method


Solution

  • Property Wrapper

    You can use property wrapper. Imagine this as an example:

    @propertyWrapper
    struct SomeKindOfBool: Decodable {
        var wrappedValue: Bool?
        
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if let stringifiedValue = try? container.decode(String.self) {
                switch stringifiedValue.lowercased() {
                case "false": wrappedValue = false
                case "true": wrappedValue = true
                default: wrappedValue = nil
                }
            } else {
                wrappedValue = try? container.decode(Bool.self)
            }
        }
    }
    

    Usage

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

    You can use someKey value like a normal Bool now:

    Test:

    let jsonData = """
    [
     { "someKey": "something else" },
     { "someKey": "true" },
     { "someKey": true }
    ]
    """.data(using: .utf8)!
    
    let decodedJSON = try! JSONDecoder().decode([MyType].self, from: jsonData)
    
    for decodedType in decodedJSON {
        print(decodedType.someKey)
    }
    

    Result:

    nil

    Optional(true)

    Optional(true)


    You can do similar for other situations and also any other type you need. Also note that I have changed the code to match your needs, but you can use the more compatible version that I posted as a gist here in GitHub