[Pre-Pitch] Codable Customization using PropertyWrappers

I understand the observed behavior and why it happens. My point is from a User POV wrapping a property shouldn't break decoding.

My concern with that is it's not done by Decodable, it's done by the implementation. I believe a custom version could already do this if desired, (besides the fact that it may break things). I don't think trying to force every Decoder implementation to use a specific approach is good solution.

Any thoughts on how to do this? @propertyWrapper? and @propertyWrapper([.optional]) are what first comes to mind.

Could a check be generated as part of the set?

set { 
    if _property == nil {
        _property = Wrapper(wrappedValue: newValue)
    }
    else { _property.wrappedValue = newValue }
}

_property.wrappedValue = newValue }

I actually find the problem of property-wrapped optional values not being decodable from omitted keys to be satisfactorily addressed by the forthcoming property wrapper composition feature and a property wrapper like @Omittable whose only purpose is to trigger a decoder overload that populates it’s wrapped value with nil if the respective key is not found. The nice thing about this is that you no longer have ambiguity by default (that an optional property can be decoded from null or from an omitted key/value).

[EDIT] I am not talking about overriding init(from: Decoder)

Another property wrapper doesn't solve the feature by itself (See @gwendal.roue's example). Changing Decoder would work, but a believe any change would require support be added to each individual decoder implementation. That would be a source breaking change which is not ideal. :disappointed:

Since code generation is already being used for Codable conformance and Property Wrapper I don't see I reason why it can't be used to solve this problem as well.

I’ve implemented @Omittable before, it just was not of much use to me without property wrapper composition. The key bit of machinery is

public protocol OmittableType {
    static var nilValue: Self { get }
}

extension KeyedDecodingContainer {
    public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T where T : Decodable, T: OmittableType {
        return try decodeIfPresent(T.self, forKey: key) ?? T.nilValue
    }
}

The rest is contained within a very thin

@propertyWrapper
public struct Omittable<T>: OmittableType { ... }
3 Likes

TIL that code synthesized for Codable is as malleable as other pieces of code. Thanks!

I've rewritten my sample code above, with full support for missing values:

It works!
import Foundation

// A basic property wrapper
@propertyWrapper struct Wrapped<T> {
    var wrappedValue: T
}

// Make Wrapped support Decodable
extension Wrapped: Decodable where T: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode(T.self)
    }
}

// Make Wrapped support decoding nil and missing values
protocol OmittableType {
    static var nilValue: Self { get }
}
extension Wrapped: OmittableType where T: ExpressibleByNilLiteral {
    static var nilValue: Self { .init(wrappedValue: nil)}
}
extension KeyedDecodingContainer {
    func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T where T : Decodable, T: OmittableType {
        return try decodeIfPresent(T.self, forKey: key) ?? T.nilValue
    }
}

// Setup
let decoder = JSONDecoder()
let jsonWithValue = #"{"property":"Hello"}"#.data(using: .utf8)!
let jsonWithNull =  #"{"property":null}"#.data(using: .utf8)!
let jsonEmpty =     #"{}"#.data(using: .utf8)!

// A regular decodable struct: full success
struct Struct: Decodable {
    var property: String?
}
try! decoder.decode(Struct.self, from: jsonWithValue).property        // "Hello"
try! decoder.decode(Struct.self, from: jsonWithNull).property         // nil
try! decoder.decode(Struct.self, from: jsonEmpty).property            // nil

// A decodable struct with wrapped property: full success
struct WrappedStruct: Decodable {
    @Wrapped var property: String?
}
try! decoder.decode(WrappedStruct.self, from: jsonWithValue).property // "Hello"
try! decoder.decode(WrappedStruct.self, from: jsonWithNull).property  // nil
try! decoder.decode(WrappedStruct.self, from: jsonEmpty).property     // nil
4 Likes

:astonished: That works! I didn't realize specialized functions diverted code from outside your own code...makes sense though. Thank you! I would still like a more "official" way to handle this, but this at least gives me the workaround I need!

Looks like a nifty library only not sure we need a pitch to add it to Swift's.

This workaround - could it be used to wrap an optional Bool property, so that it's serialized in JSON as an Int (1=true, 0=false or null if nil or missing) and decoded as a Bool?

I have a case where an external API, over which I have no control, returns a property as a JSON number (1 or 0) meaning true or false, and I want to treat it as an optional Bool in my Codable Swift class (I'm using a class, not a struct, as I'm integrating Codable with CoreData classes).

In Kotlin, I achieved this very easily using GSON's @JsonAdapter annotation;

data class MyDataClass (
@JsonAdapter(CustomTypeAdapterFactory::class) @SerializedName("my_optional_bool") val myOptionalBool: Boolean?
)

But I'm not sure of the best way to achieve this in Swift. I have a similar issue, where I need to encode a Date into a custom JSON format, that I would like to achieve in a similar way.

Customizing Codable with PropertyWrappers seems to me like the closest thing I've seen so far, but it would be good to get guidance on whether this is the case.

Yes, it should be possible. I'm planning on pushing the nullable wrappers this weekend to make it easier, but the non-null version would be something like this:

struct IntAsBoolStaticCoder: StaticCoder {

    static func decode(from decoder: Decoder) throws -> Bool {
        let intValue = try Int(from: decoder)
        return intValue > 0 ? true : false
    }

    static func encode(value: Bool, to encoder: Encoder) throws {
        (value ? 1 : 0).encode(to: encoder
    }
}

struct MyType: Codable {
    @CodingUses<IntAsBoolStaticCoder>
    var myBool: Bool
}

I'll add it as a custom example here as well when it's finished.

Thanks for this. Your library is excellent, exactly the kind of thing I was looking for! I look forward to the update to nullable wrappers.

Another useful concept in my Kotlin-based data classes, that use Android's Room library, is the @Expose annotation - ie. that can be used for deciding which properties get serialized to JSON.

At the moment, I'm planning to handle this in Swift by omitting CodingKeys for the properties I don't want serialized (I understand this is currently the standard way to do this) - but I find this a bit ugly and non-intuitive. It seems to me it would be better handled by property wrappers, either by exposing or ignoring the property where it's declared, e.g.;

struct MyType: Codable {
    @CodingUses<IntAsBoolStaticCoder>
    var myBool: Bool
    @Ignore
    var myStringNotToBeSerialized: String
}

Whether this should be handled by a library like yours, or whether this should be doable in standard Swift as part of customizing Codable, I don't know - it's just something I'd like to be able to do, along with using a GSON-like @SerializedName 'annotation' instead of, again, defining it within the CodingKeys enum, although I understand a property can only have one property wrapper (?), so I guess it's not doable to use multiple property wrappers to achieve something like;

 struct MyType: Codable {
    @SerializedName("my_date") @SecondsSince1970DateCoding
    var myDate: Date
 }

Should it be more generalized:

struct IntegerAsBoolStaticCoder<Integer: BinaryInteger & Codable>: StaticCoder {

    static func decode(from decoder: Decoder) throws -> Bool {
        let intValue = try Integer(from: decoder)
        return intValue != 0
    }

    static func encode(value: Bool, to encoder: Encoder) throws {
        try Integer(value ? 1 : 0).encode(to: encoder)
    }

}

I wonder if it's possible for a property wrapper to have a generic parameter for another PW so the outer one channels both. Hmm, but how would initializations work (if the inner one needs to)?

Thanks for encouragement!

That's what composition is supposed to handle (see proposal) but as it's missing from Swift 5.1 even though the proposal says "implemented", I'm unsure when/if that's coming.

Property wrapper composition is available on the master branch tool chain. :blush:

1 Like

Niiice! Hopefully a release with it will happen soon :slight_smile:

@andrewayres I've now released v1.1.0.

With this we can now easily implement your use case (also tested it as a class just to make sure)

struct IntAsBoolStaticCoder: StaticCoder {

    static func decode(from decoder: Decoder) throws -> Bool {
        let intValue = try Int(from: decoder)
        return intValue > 0 ? true : false
    }

    static func encode(value: Bool, to encoder: Encoder) throws {
        try (value ? 1 : 0).encode(to: encoder)
    }
}

typealias IntAsBoolOptionalCoding = CodingUses<OptionalStaticCoder<IntAsBoolStaticCoder>>

struct MyType: Codable {
    @IntAsBoolOptionalCoding
    var myBool: Bool?
    @OmitCoding
    var myStringNotToBeSerialized: String?
}

I'm also thinking it makes sense to add some similar Bool wrappers next time I get chance.

2 Likes

@GetSwifty - That's awesome! Thanks for releasing v.1.1.0, including @OmitCoding, and testing with a class too.

However, I've run into an issue with CoreData class properties that use the @NSManaged attribute

public class MyCoreDataClass: NSManagedObject {   
    ...
    @NSManaged public var myString: String?
}

Trying to add a Property Wrapper (either before or after @NSManaged), e.g.;

@OmitCoding @NSManaged public var myString: String?
or
@NSManaged @OmitCoding public var myString: String?

results in the Xcode error;

Property 'myString' with a wrapper cannot also be @NSManaged.

I'm currently looking for a workaround to this.

That's not too surprising. There are a lot of Swift features that don't work well with Core Data. That's why I tend to use separate models for the Codable part of a codebase.

1 Like

@GetSwifty have you looked further into the @CodingKey("first-name") syntax? I'd be interested in this for decoding JSON that does not exactly match my codable struct.

Additionally, though this is more out of your proposal's realm, it would be interesting if we could define multiple decoding strategies so that we could decode our objects from different JSON sources for example. My use case is I have one model that I'd like to decode from some seed data and then decode the same model from a different api.