PropertyWrapper, Optional, and Codable

I'm using property wrapper to serialzie Decimal into String. My code works fine for types like Decimal and Dictionary containg Decimal, but fails with Decimal?. I have read related discussion here and here. I have a workaround, but I suspect the decoding failure is a bug in codable sythesizer and wonder if there is a way to avoid the issue in the first place.

Below is the code to demonstrate the issue (it's a bit long, but should be simple to follow).

First, this is my property wrapper. I introduced a protocol because I'd like to support not only Decimal but also Dictionary or Enum containing Decimal.

protocol PreciseDecimalWrappedValue: Codable {
    associatedtype StorageType: Codable

    func toStorageValue() -> StorageType
    // This throws when we get a value of StorageType but fail to convert it.
    static func fromStorageValue(_ storageValue: StorageType) throws -> Self
}

@propertyWrapper
struct PreciseDecimal<T: PreciseDecimalWrappedValue>: Codable {
    var wrappedValue: T

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

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        let storageValue = try container.decode(T.StorageType.self)
        self.wrappedValue = try T.fromStorageValue(storageValue)
    }

    func encode(to encoder: Encoder) throws {
        try wrappedValue.toStorageValue().encode(to: encoder)
    }
}

Below is an example usage:

struct SomeError: Error {
}

extension Dictionary: PreciseDecimalWrappedValue where Key: Codable, Value == Decimal {
    typealias StorageType = [Key: String]

    func toStorageValue() -> StorageType {
        mapValues { decimal in
            decimal.description
        }
    }

    static func fromStorageValue(_ storageValue: StorageType) throws -> Self {
        try storageValue.mapValues { stringValue in
            guard let decimal = Decimal(string: stringValue) else {
                throw SomeError()
            }
            return decimal
        }
    }
}

// Test code

func encodeAndDecode<T: Codable>(_ t: T) throws -> T {
    let data = try! JSONEncoder().encode(t)
    let value = try JSONDecoder().decode(T.self, from: data)
    return value
}

struct Test1: Codable {
    @PreciseDecimal var x: [String: Decimal] = ["a": 1.1, "b": 2.2, "c": 3.3]
}

print(try encodeAndDecode(Test1()).x)

I use SingleValueContainer in the property wrapper. According to the doc, it should only be used for primitive type. I'm not sure if Dictionary or Enum is considered as primitive type or not (does anyone know it?), but my experiments show the code works fine with them. That said, I suspect it's the use of SingleValueContainer that causes the issue with Decimal?. Please read on.

To support using the property wrapper with Decimal?, I added the following code:

extension Decimal: PreciseDecimalWrappedValue {
    typealias StorageType = String

    func toStorageValue() -> StorageType {
        self.description
    }

    static func fromStorageValue(_ storageValue: StorageType) throws -> Self {
        guard let decimal = Decimal(string: storageValue) else {
            throw SomeError()
        }
        return decimal
    }
}

extension Optional: PreciseDecimalWrappedValue where Wrapped: PreciseDecimalWrappedValue {
    typealias StorageType = Optional<Wrapped.StorageType>

    func toStorageValue() -> StorageType {
        switch self {
        case .none:
            return .none
        case .some(let wrapped):
            return .some(wrapped.toStorageValue())
        }
    }

    static func fromStorageValue(_ storageValue: StorageType) throws -> Self {
        switch storageValue {
        case .none:
            return .none
        case .some(let wrapped):
            return .some(try Wrapped.fromStorageValue(wrapped))
        }
    }
}

But unfortunately it fails to decode nil value.

struct Test2: Codable {
    @PreciseDecimal var x: Decimal?
}

// this works fine
print(try encodeAndDecode(Test2(x: 10)).x)

// this crashed with "Expected Optional<String> but found null value instead." error
print(try encodeAndDecode(Test2(x: nil)).x)

There are two parts involved with regard to the failure. The first part is the encoding part, which encodes Test2(x: nil) to {"x":null}. The second part is the decoding part, which generates the error.

I think I understand the encoding part. Codable synthesizer discards a nil property value by default when doing encoding. But according to explanation in this thread, using property wrapper has an unexpected side effect: from the codable synthesizer's perspective, it changes an optional property to a non-optional property. That, plus the fact that I use SingleValueContainer in the property wrapper, is the reason I got {"x":null}, instead of {}.

I don't understand the failure in the decoding part. The error message suggested that the codable synthesizer can't decode null to Optional<String>. This is something I'm not sure, because from my understanding SingleValueContainer should be able to decode null to Optional<String>. Actually the null value in JSON data is the SingleValueContainer encoding output of nil value of Optional<String> type. How come it only works one way and not the other way around? Could this be a bug?

Now let's suppose the decoding failure isn't a bug, the question is if there is any way to generate {}, instead of {"x":null}, when using property wrapper? EDIT: on a second thought, even if I could find something way to generate {}, it wouldn't work with SingleValueContainer because the decoding would still fail - SingleValueContainer expect some input, instead of nothing, when doing decoding. So the issue is unsolvable with SingleValueContainer. On the other hand, it's not obvious how I can use KeyedDecodingContainer in my property wrapper.

I'm currently using this work around (I find it in the links I mentioned above). But I wonder if it's possible to avoid the issue in the first place?

extension KeyedDecodingContainer {
    func decode<T: ExpressibleByNilLiteral>(_ type: PreciseDecimal<T>.Type, forKey key: K) throws -> PreciseDecimal<T> {
        if let value = try self.decodeIfPresent(type, forKey: key) {
            return value
        }
        return PreciseDecimal(wrappedValue: nil)
    }
}