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?

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
  • @SPatel this service doesn't use property wrappers so it's not really helpful. – Ortwin Gentz Nov 15 '21 at 18:19
  • What happens if you change the type of someKey to be non-optional? – Joakim Danielson Nov 15 '21 at 19:20
  • Same error. And then I could no longer differentiate between false and nil (aka not existing). – Ortwin Gentz Nov 15 '21 at 20:43
  • 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. – Joakim Danielson Nov 15 '21 at 21:12
  • Have you tried with `decodeIfPresent`instead? – valeCocoa Nov 15 '21 at 23:13
  • @JoakimDanielson indeed, it fails on the last `{}`. It should set `someKey` to nil since it's an Optional. – Ortwin Gentz Nov 16 '21 at 01:05
  • 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. – Joakim Danielson Nov 16 '21 at 07:48
  • 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? – indrajit Sep 23 '22 at 13:30

1 Answers1

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?
}
Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213