Search code examples
swiftdecodableproperty-wrapper

How can I use @propertyWrapper for Decodable with optional keys?


I'm using a property wrapper to decode the strings "true" and "false" as Booleans. I also want to make the key optional. So if the key is missing from the JSON, it should be decoded as nil. Unfortunately, adding the property wrapper breaks this and a Swift.DecodingError.keyNotFound is thrown instead.

@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)
        }
    }
}

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

let jsonData = """
[
 { "someKey": true },
 { "someKey": "false" },
 {}
]
""".data(using: .utf8)!

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

for decodedType in decodedJSON {
    print(decodedType.someKey ?? "nil")
}

Any idea how to resolve this?


Solution

  • The synthesized code for init(from:) normally uses decodeIfPresent when the type is optional. However, property wrappers are always non-optional and only may use an optional as their underlying value. That's why the synthesizer always uses the normal decode which fails if the key isn't present (a good writeup in the Swift Forums).

    I solved the problem by using the excellent CodableWrappers package:

    public struct NonConformingBoolStaticDecoder: StaticDecoder {
        
        public static func decode(from decoder: Decoder) throws -> Bool {
            if let stringValue = try? String(from: decoder) {
                switch stringValue.lowercased() {
                case "false", "no", "0": return false
                case "true", "yes", "1": return true
                default:
                    throw DecodingError.valueNotFound(self, DecodingError.Context(
                        codingPath: decoder.codingPath,
                        debugDescription: "Expected true/false, yes/no or 0/1 but found \(stringValue) instead"))
                }
            } else {
                return try Bool(from: decoder)
            }
        }
    }
    
    typealias NonConformingBoolDecoding = DecodingUses<NonConformingBoolStaticDecoder>
    

    Then I can define my decodable struct like this:

    public struct MyType: Decodable {
        @OptionalDecoding<NonConformingBoolDecoding> var someKey: Bool?
    }