Codable synthesis and Property Wrappers with initialization arguments

Take for example the Clamping<V: Comparable> and Color examples from SE-0258. What if we wanted to make that structure Decodable.

struct Color: Decodable {
  @Clamping(min: 0, max: 255) var red: Int = 127
  @Clamping(min: 0, max: 255) var green: Int = 127
  @Clamping(min: 0, max: 255) var blue: Int = 127
  @Clamping(min: 0, max: 255) var alpha: Int = 255
}

You'll first hit an error that the compiler can't synthesize the conformance because Clamping<Int> doesn't conform to Decodable. Ah, so maybe if I make this property wrapper Decodable, everything will all work fine.

extension Clamping: Decodable where V: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.value = try container.decode(V.self)
        // error: Return from initializer without initializing all stored properties
    }
}

If you try something simple, like trying to forward decoding down to the wrapped value's single value container, you realize just this won't exactly work. You don't know what you'd set to self.min and self.min at this layer. So this direction isn't going to work.

But, if you actually just write your own init(from:), it's all fine.

extension Color: Decodable {
    enum CodingKeys: CodingKey {
        case red, green, blue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.red = try container.decode(Int.self, forKey: .red)
        self.green = try container.decode(Int.self, forKey: .green)
        self.blue = try container.decode(Int.self, forKey: .blue)
    }
}

It does because self._red = Clamping(wrappedValue: 255, min: 0, max: 255) is implicitly initialized as default property. Here the min/max values are passed along and available for re-assignment when the value is decoded.

I suppose this behavior could be related to fact that other synthesized initializers also do not initialize default values.

Does this sound correct?

2 Likes

Realized my misconception. The compiler isn't actually trying to synthesize what I wrote above.

Synthesis for Encodable , Decodable , Hashable , and Equatable use the backing storage property. This allows property wrapper types to determine their own serialization and equality behavior. For Encodable and Decodable , the name used for keyed archiving is that of the original property declaration (without the _ ).

https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#codable-hashable-and-equatable-synthesis

What I believe this paragraph is actually saying is that something more like this is going on:

extension Color: Decodable {
    enum CodingKeys: CodingKey {
        case red, green, blue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self._red = try container.decode(Clamping<Int>.self, forKey: .red)
        self._green = try container.decode(Clamping<Int>.self, forKey: .green)
        self._blue = try container.decode(Clamping<Int>.self, forKey: .blue)
    }
}

With this you'll end up with that original error that requires that 'Clamping<Int>' conform to 'Decodable'. So this seems to be the expected behavior and aligns with that paragraph.

Though, it isn't a useful default synthesize in this use case as it's not possible to reasonably make Clamping: Decodable.

import PlaygroundSupport
import Foundation

@propertyWrapper struct Clamping<V> where V: Comparable {
    var wrappedValue: V {
        didSet {
            wrappedValue = min(maxi, max(mini, wrappedValue))
        }
    }

    let mini: V
    let maxi: V

    init(wrappedValue: V, min: V, max: V) {
        self.mini = min
        self.maxi = max
        self.wrappedValue = Swift.min(maxi, Swift.max(mini, wrappedValue))
    }
}

extension Clamping: Codable where V: Codable {

}

struct S: Codable {
    @Clamping(min: 3, max: 7) var x: Int = 8
}

let d = try! JSONEncoder().encode(S())

var s = try! JSONDecoder().decode(S.self, from: d)

print(s) // S(_x: __lldb_expr_10.Clamping<Swift.Int>(wrappedValue: 7, mini: 3, maxi: 7))
1 Like

That example encodes the static clamping bounds into the JSON itself.

print(String(decoding: d, as: UTF8.self))
// {"x":{"mini":3,"maxi":7,"wrappedValue":7}}

Right. I just don't understand how you would expect it to work any other way. If you're using a property wrapper that has state, it either has to explicitly discard that state when encoding, or it has to encode it. And if the former, the wrapper will have to have a way to regenerate it when decoded.

You wrote:

and then proceeded to write an implementation which implies that one should encode to a single-value container, without justifying that assumption.

That is not always desirable, I agree.

The property declaration no longer provides an accurate description of the clamping:

struct S: Codable {
    // Well... 3 and 7, unless it isn't 3 and 7
    @Clamping(min: 3, max: 7) var x: Int = 8
}

This may fit some apps, but it does not fit all apps.

Some apps want the clamping to be defined in source code, not in the encoded representation.

2 Likes

I don't understand.

That was my quick test to prove to myself that the wrapper works as intended. It has absolutely nothing to do with Codable.

I am not positive I understand what you're getting at, but I wrote in my previous reply

If one is going that route, one has to be able to provide those values when decoding. Expecting Decodable to magically resuscitate them does not seem reasonable to me.

I'm not 100% sure I follow what you're trying to do, but there's no need to use containers:

extension Clamping: Decodable where V: Decodable {
    init(from decoder: Decoder) throws {
        self.value = try V(from: decoder)
    }
}

Your example assumes that the only property of Clamping which is encoded is wrappedValue. The trouble seems to be that decoding can't bring back the min and max values the wrapper must be initialized with.

Ah yeah, looked it things a bit more closely. Was thrown off by the version in the OP.

@joshpeek If you want to decode a PropertyWrapper all the info needs to be statically available. That's not possible when passing your min and max.

Two options come to mind. Make a version of Clamping where the values can be injected statically:

protocol MinMaxProvider {
    associatedtype ValueType
    static var min: ValueType { get }
    static var max: ValueType { get }
}

@propertyWrapper struct StaticClamping<ConstraintProvider: MinMaxProvider> where ConstraintProvider.ValueType: Comparable {
    var wrappedValue: ConstraintProvider.ValueType {
        didSet {
            wrappedValue = min(ConstraintProvider.max, max(ConstraintProvider.min, wrappedValue))
        }
    }

    init(wrappedValue: ConstraintProvider.ValueType) {
        self.wrappedValue = Swift.min(ConstraintProvider.max, Swift.max(ConstraintProvider.min, wrappedValue))
    }
}

extension StaticClamping: Decodable where ConstraintProvider.ValueType: Decodable {
    init(from decoder: Decoder) throws {
        let value = try ConstraintProvider.ValueType(from: decoder)
        self.wrappedValue = Swift.min(ConstraintProvider.max, Swift.max(ConstraintProvider.min, value))
    }
}

extension StaticClamping: Encodable where ConstraintProvider.ValueType: Encodable {
    func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
}

struct HexProvider: MinMaxProvider {
    static var min: Int = 0
    static var max: Int = 255
}

typealias HexClamping = StaticClamping<HexProvider>

struct Color: Codable {
  @HexClamping var red: Int = 127
  @HexClamping var green: Int = 127
  @HexClamping var blue: Int = 127
  @HexClamping var alpha: Int = 255
}

Or since you're reusing the same values, just make a wrapper for that specifically:

@propertyWrapper struct HexClamping: Codable {
    let min = 0
    let max = 255
    var wrappedValue: Int {
        didSet {
            wrappedValue = Swift.min(max, Swift.max(min, wrappedValue))
        }
    }

    init(wrappedValue: Int) {
        self.wrappedValue = Swift.min(max, Swift.max(min, wrappedValue))
    }

    init(from decoder: Decoder) throws {
        let value = try Int(from: decoder)
        self.wrappedValue = Swift.min(max, Swift.max(min, value))
    }

    func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
}
struct Color: Codable {
  @HexClamping var red: Int = 127
  @HexClamping var green: Int = 127
  @HexClamping var blue: Int = 127
  @HexClamping var alpha: Int = 255
}

This results in encoding/decoding JSON in this format:

{"red":127,"alpha":255,"blue":127,"green":127}

Note the custom Codable implementation is necessary otherwise the encode/decode will be looking for

{ "red": { "wrappedValue": 127 } }

@GetSwifty thanks for the suggested work around. I was aware making those values static could be a workaround, but you lose that customization flexibility. I now realize why your CodableWrappers library only uses static types as well. I'm guessing you bumped into this same limitation :wink:

Yeah, still noodling on some improvements with Swift 5.2 but it hasn't been too fruitful :disappointed:

Good luck!

Yes, I have the same problem. For reasons outside my control, I have to deal with an API which has missing values for many of its fields when they contain what the API considers to be a default value.

That is, some json structures have missing field instead of an empty array, a missing field instead of a enum value, etc. I've managed to solve this for the most part using property wrappers and defining the default values as static properties on the types.

This looks like this (not complete):

struct Employee: Decodable {
  @DefaultValueDecodable public var role: EmployeeRole
  @DefaultValueDecodable public var subordinates: [Employee] 
  // ... and so on
}

protocol Defaultable: {
  static var defaultValue: Self { get }
}

@propertyWrapper
struct DefaultValueDecodable<Value: Decodable & Defaultable>: Decodable { /* ... */ }

Is is easy to make [] be the default for Array, and 0 for Int, etc.
However, for Bool, I sometimes want false, but other times true to be the default value.

I've solved it, by chaging the Defaultable protocol:

protocol Defaultable: {
  associatedtype DefaultType
  static var defaultValue: DefaultType { get }
}

@propertyWrapper
struct DefaultValueDecodable<Value: Decodable, Default: Defaultable>: Decodable
  where Default.DefaultType == Value { /* ... */ }

That way, I can define a phantom type like so:

enum True: Defaultable {
  static let defaultValue = true
}

struct Employee: Decodable {
  @DefaultValueDecodable public var role: EmployeeRole
  @DefaultValueDecodable public var subordinates: [Employee] 
  @DefaultValueDecodable(True.self) public var isActive: Bool
  // ... and so on
}

However, for some strings, it becomes more and more unwieldy to add these value-as-a-type phantom types. I just want to do:

struct Employee: Decodable {
  @DefaultValueDecodable(default: .none) 
  public var role: EmployeeRole

  @DefaultValueDecodable(default: []) 
  public var subordinates: [Employee] 

  @DefaultValueDecodable(default: true) 
  public var isActive: Bool
  // ... and so on
}

The alternative is to write a bespoke init(from:) for each model, but that also becomes very tedious when there are many fields, and most of them are trivial.

I which property wrappers could satisfy its Decodable conformance with this:

// this would be used in synthesized decodable 
init(foo: Foo, bar: Bar, from: Decoder) throws {}

// if this was provided
init(foo: Foo, bar: Bar, wrappedValue: Value) throws {}
1 Like
Terms of Service

Privacy Policy

Cookie Policy