Property Wrappers that can be updated from a decoder

The codable protocols are a really great abstraction for dealing with serializing models to and from data. The default implementation is perfect, if your model already happens to nicely match the structure your data is in. If your data is in a completely different structure, custom implementations of the codable protocol methods give you the options to do anything you need. The place where it is still frustrating is when your model almost matches the structure of your data. As soon as anything is out of place you need to go with the full custom implementation.

Property Wrappers feel like they should be the solution to this, but there are just enough sticking points with making property wrappers decodable that they can't quite work for the things I see needed most often.

Currently when making a property wrapper decodable the wrapper is instantiated once and then a separate one is instantiated during init(from: decoder)

@propertyWrapper
struct DecodableWrapper<T: Decodable>: Decodable {
    var wrappedValue: T?
    var someOtherValue: String

    init(someOtherValue: String) {
        print("In first init")
        self.someOtherValue = someOtherValue
    }

    init(from decoder: Decoder) throws {
        print("In init with decoder")
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode(T.self)
        someOtherValue = "new, different value" // No longer have access to the original `someOtherValue`
    }
}

struct SomeModel: Decodable {
    @DecodableWrapper(someOtherValue: "Initial") var thing: String?
}

When decoding an instance of SomeModel the output would be:

In first init
In init with decoder

and as noted in the comments, when you hit the init(from: decoder) you no longer have any access to the someOtherValue passed into the first init.

If there was a way to make property wrappers able to update themselves from a decoder instead, this would open up a lot more options and seems like it should be something that could be purely additive (I'm ignorantly hoping...)

protocol DecoderUpdateable {
    mutating func update(from: Decoder) throws
}

@propertyWrapper
struct Default<T: Decodable>: DecoderUpdateable {
    var wrappedValue: T

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

    mutating func update(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.self)) ?? wrappedValue
    }
}

struct SomeModel: Decodable {
    @Default(value: "Some Fallback") var thing: String
}

There would be use cases outside of property wrappers where something like this would be useful as well, where types can't be instantiated as part of the decoding process (like CoreData entities)

But in the case of property wrappers specifically I think that this wouldn't be quite enough. The problem gets a little harder (or at least my solutions get more awkward) due to the fact that if that key is missing, the way decoding works now an error would get thrown for the missing key before we'd ever get to the update(from: Decoder) function. There are hackarounds for this behavior now for decodable property wrappers, where you can override KeyedDecodingContainer methods example (thanks to @Ian_Keen for showing me this and having gists like this up)

Most of the places where I find I have to give up on the default implementation of init(from: Decoder) and would love a single property wrapper for instead of a full custom implementation would fail because of this missing key problem:

  • A key I want to ignore when decoding and just take a value
  • I want to use a different property name than the key in the data
  • I am trying to take properties from the root level of the object and make them a nested object (like taking a latitude and longitude and making them into a coordinate)

For all of these it would be nice if we could somehow get access to the decoder for the whole parent object, not just a decoder for a single value container, and also having the key that would have been used to decode would be helpful. Something like:

protocol DecodingPropertyWrapper {
    mutating func update<Key: CodingKey>(from: Decoder, forKey: Key) throws
}

@propertyWrapper
struct Default<T: Decodable>: DecodingPropertyWrapper {
    var wrappedValue: T

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

    mutating func update<Key: CodingKey>(from decoder: Decoder, forKey key: Key) throws {
        let container = try decoder.container(keyedBy: Key.self)
        wrappedValue = try container.decodeIfPresent(T.self, forKey: key) ?? wrappedValue
    }
}

@propertyWrapper
struct DontDecode<T: Decodable>: DecodingPropertyWrapper {
    var wrappedValue: T

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

    mutating func update<Key: CodingKey>(from decoder: Decoder, forKey key: Key) throws {
        // No Op
    }
}

@propertyWrapper
struct Key<T: Decodable>: DecodingPropertyWrapper {
    private let key: AnyCodingKey
    var wrappedValue: T!

    init(_ key: String) {
        self.key = AnyCodingKey(key)
    }

    mutating func update<Key: CodingKey>(from decoder: Decoder, forKey key: Key) throws {
        let container = try decoder.container(keyedBy: AnyCodingKey.self)
        wrappedValue = try container.decode(T.self, forKey: self.key)
    }
}

struct SomeModel: Decodable {
    @Default(value: "Some Fallback") var thing: String
    @DontDecode(value: 5) var number: Int
    @Key("some_weird-key_iDontWantToUse") var title: String
}

(AnyCodingKey also from Ian)

The method on this DecodingPropertyWrapper protocol is definitely more awkward and less self-explanatory than a simple update(from: Decoder) but I think if we had those 2 pieces of information at that update point, almost any one-off data situation would be solvable with a property wrapper instead of needing a full custom decoder implementation. Having a protocol like this would involve needing to add a new protocol and new code-gen rules for the default decodable implementation, but shouldn't need any source breaking changes to anything codable. Not sure if I'm going about this the wrong way and there are better ways of addressing the almost-decodable pain points, but wanted to at least lob this up for discussion.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy