2

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 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 primitive types should be able to handle it and if not then it should be set to nil provided 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 Bool1: 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()
            }
        }
    }
}

but this created 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

Community
  • 1
  • 1
Kaunteya
  • 3,107
  • 1
  • 35
  • 66
  • How about this? https://stackoverflow.com/a/46850257/562298 (Create a custom wrapper for your custom type, and then adding a computed property that calls through to xyz.bool, so devs don't need to be confused.) – Sebastian Dec 04 '19 at 20:57
  • Ya. That is the solution that I have implemented right now. But it has some downsides. 1. All the developers need to use wrapper types, which does not look intuitive 2. All variables would need extra function to extract the actual value. – Kaunteya Dec 05 '19 at 06:17
  • *Backend cannot be relied upon for types.* Why? That's ridiculous. The backend is supposed to make the effort for perfomance reasons, not the frontend. – vadian Dec 08 '19 at 09:41
  • I suppose `JSONSerialization` doesn't work in your case? – Alex Dec 10 '19 at 03:52

1 Answers1

11

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

Community
  • 1
  • 1
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • Hm, it seems this solution doesn't work for non-existing keys. I'd like to decode such keys to nil. When using the property wrapper, a `Swift.DecodingError.keyNotFound` is thrown instead. – Ortwin Gentz Nov 15 '21 at 15:52
  • Yep, you shod change the code for supporting optionals too. I had that but not on the gist. You can ask a question so The community and I can answer that specific question there. – Mojtaba Hosseini Nov 15 '21 at 17:29
  • Thanks, I've posted a new question: https://stackoverflow.com/q/69978668/235297 – Ortwin Gentz Nov 15 '21 at 17:48