1

Let's say I have decodable property wrapper:

@propertyWrapper
struct OptionalDecodable<Value: Decodable>: Decodable {
  var wrappedValue: Value?
}

The compiler does synthesize init for the following

struct Model: Decodable {
  @OptionalDecodable private(set) var string: String?
}

To test if this works I just try to decode empty JSON (i.e. "{}")

However, string property is not treated as optional, i.e. when there's no string key I get an error that key was not found.

Is there a work around this?

Ihar
  • 13
  • 3
  • 1
    Not clear to me what you're asking. Can you add to the question the JSON that you're trying to decode? Also, why do you need this property wrapper? Optionals are already handled by Codable – New Dev Feb 21 '21 at 14:19
  • @NewDev Updated the question, hope this helps. The purpose of this wrapper is to perform custom decoding under the hood. Say, I want strings to be decoded properly from both strings and numbers. The idea works great with non-optional data types. But when it came to optionals, I got stuck – Ihar Feb 21 '21 at 14:43

1 Answers1

0

I'm not sure if this is the best way, but the issue is that wrappedValue type of the property wrapper has to match the property's type, and String is different than String?.

One approach to overcome this is to make the property wrapper generic, but constrain in such a way that would allow you to initialize the type from a String or an Int:

protocol ExpressibleByString {
    init(fromString: String)
}
extension String: ExpressibleByString {
    init(fromString: String) { self = fromString }
}
extension Optional: ExpressibleByString where Wrapped == String {
    init(fromString: String) { self = fromString }
}
@propertyWrapper
struct IntOrString<V: ExpressibleByString & Decodable>: Decodable {
    var wrappedValue: V
}

extension IntOrString {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        
        do {
            let int = try container.decode(Int.self)
            wrappedValue = .init(fromString: int.description)
        } catch DecodingError.typeMismatch {
            wrappedValue = try .init(fromString: container.decode(String.self))
        }
   }
}

extension KeyedDecodingContainer {
    func decode<V: ExpressibleByNilLiteral>(_ t: IntOrString<V>.Type, forKey key: K) throws -> IntOrString<V> {
        if let v = try decodeIfPresent(t, forKey: key) {
            return v
        }
        return IntOrString(wrappedValue: nil)
    }
}

Then you could use it on both optional and non-optional String:

struct Foo: Decodable {
    @IntOrString
    var p1: String?

    @IntOrString
    var p2: String
}
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • Okay, so I've written a test for your code: `let test = #"{ "p2": "value" }"# let data = test.data(using: .utf8)! do { let object = try JSONDecoder().decode(Foo.self, from: data) print(object) } catch { print(error) }` As you can see, property `p1` is missing in the test JSON. And the output is `keyNotFound(CodingKeys(stringValue: "p1", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"p1\", intValue: nil) (\"p1\").", underlyingError: nil))` Which is the issue I am trying to resolve – Ihar Feb 22 '21 at 03:34