propertyWrapper decoding behaviour

I recently came across a behaviour when using propertyWrappers with the Codable protocol which I find strange, so I wanted to ask if that is intended. So lets assume I have a model as follows:

struct Person: Codable {
 var name: String?
}

The following can be decoded without a problem with the compiler synthesised Decodable implementation:

{
}

Now I want to use a simple propertyWrapper as follows:

@propertyWrapper
struct Passthrough<T: Codable>: Codable {
 var wrappedValue: T
}

Applying the propertyWrapper to my model as follows:

struct Person: Codable {
 @Passthrough var name: String?
}

Now if I try to decode the payload again it fails with the error:

keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil)

I am wondering if that's a bug because my expectation was that the decoding would apply to the wrappedValue type, if not is there a way to make the described scenario work without having to manually implement the initialiser of Person?

UPDATE:
Even if I manually implement Codable, the Decodable architecture treats the propertyWrapper as if I wanted to decode the propertyWrapper, but that's not what I want.
Here is what I came up with:

@propertyWrapper
struct Passthrough<T: Codable>: Codable {

    var wrappedValue: T

    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.wrappedValue = try container.decode(T.self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}
1 Like

I agree that using @propertyWrapper is a great way to customise the behaviour of Codable in a declarative manner. There are a few tricks required which make sense when you think about what the compiler is doing for us.

The wrapper must conform to Codable because it is encoded and decoded, but you probably would like to omit its synthesised container from encoding {"wrappedValue": ....} and skip directly to the wrapped value using singleValueContainer()

@propertyWrapper
struct Passthrough<T: Codable>: Codable {
  var wrappedValue: T

  init(wrappedValue: T) {
    self.wrappedValue = wrappedValue
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    self.wrappedValue = try container.decode(T.self)
  }
}

Next when the compiler synthesises the decoding of struct Person it looks like this:

init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  _foo = try container.decode(Passthrough<String?>.self, forKey: .foo)
}

This fails when CodingKeys.foo does not exist within the container so we should use decodeIfPresent() instead.

While we cannot change the synthesised code at all we can add a specialised extension to KeyedDecodingContainer that the synthesised code will call instead.

extension KeyedDecodingContainer {
  func decode<T>(_ type: Passthrough<T?>.Type, forKey key: Key) throws -> Passthrough<T?> {
    try decodeIfPresent(type, forKey: key) ?? Passthrough<T?>(wrappedValue: nil)
  }
}

This extension will appear to be dead code but the synthesised init(from decoder: Decoder) will be calling it.

6 Likes

I like that solution thank you! So that essentially means I have to have an extension on the KeyedDecodingContainer for every propertyWrapper that I want to work with Codable correct?

Yes — for every @propertyWrapper that wraps an Optional or decodes null into something more useful:

// decode null as empty array []
@OptionalArray var elements: [Int] 
1 Like