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

1

1

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?

Especially answered 15/11, 2021 at 17:47 Comment(8)
@SPatel this service doesn't use property wrappers so it's not really helpful.Especially
What happens if you change the type of someKey to be non-optional?Sisson
Same error. And then I could no longer differentiate between false and nil (aka not existing).Especially
Ok, I tried running your code and it fails on the last entry in your json, {}, which I am not sure what it's supposed to be? And next time you post an error message then post the full message.Sisson
Have you tried with decodeIfPresentinstead?Engrossing
@JoakimDanielson indeed, it fails on the last {}. It should set someKey to nil since it's an Optional.Especially
But it doesn’t matter if someKey is optional since that mean that the value is optional, here we have neither key nor value. So the last {} means an empty object but nothing about its type.Sisson
If I implement decode method in MyType then it does not forward control to PropertyWrapper's decoding method. In some cases we are decoding properties manually using container and custom keys. Any solution?Infringe
E
0

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?
}
Especially answered 16/11, 2021 at 2:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.