propertyWrapper decoding behaviour

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.

7 Likes