Optional property wrapper used in Codable struct fails when value is missing

Viewed 1804

I've got a struct with a Double property that is represented as a String in the JSON coming from the backend.

struct Test: Codable {
    @StringRepresentation
    var value: Double?
}

Instead of implement init(from:) I've created the following property wrapper that takes advantage of LosslessStringConvertible to convert to and from String

@propertyWrapper
struct StringRepresentation<T: LosslessStringConvertible> {
    private var value: T?

    var wrappedValue: T? {
        get {
            return value
        }
        set {
            value = newValue
        }
    }
}

extension StringRepresentation: Codable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if container.decodeNil() {
            value = nil
        } else {
            let string = try container.decode(String.self)
            value = T(string)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        if let value = value {
            try container.encode("\(value)")
        } else {
            try container.encodeNil()
        }
    }
}

This works for

{
  "value": "12.0"
}

and

{
  "value": null
}

but fails when the property is missing

{
}

giving the error

▿ DecodingError
  ▿ keyNotFound : 2 elements
    - .0 : CodingKeys(stringValue: "value", intValue: nil)
    ▿ .1 : Context
      - codingPath : 0 elements
      - debugDescription : "No value associated with key CodingKeys(stringValue: \"value\", intValue: nil) (\"value\")."
      - underlyingError : nil

I'm guessing this happens because the underlying StringRepresentation is not Optional. How can I make it optional?

Edit: Also, I have to encode the object as

{
"value": null
}

i.e the value can't be omitted.

1 Answers

Use an extension on KeyedDecodingContainer and add an overload for decode method like this:

extension KeyedDecodingContainer {

    func decode<T>(_ type: StringRepresentation <T?>.Type, forKey key: Self.Key) throws -> StringRepresentation <T?> where T : Decodable {
        return try decodeIfPresent(type, forKey: key) ?? StringRepresentation <T?>(wrappedValue: nil)
    }
}

This will make sure that your synthesised property _value which is a non-optional is always created but in case the key is not present in json it only wraps around a nil value.

This works with synthesised Codable initialiser as this overloaded decode is used instead of its generic counterpart because it has a concrete type which is preferred over a generic one if present.

Related