Possible to set default values for optional vars in Codable struct?

Looking for a convenient way to set default values on vars that are part of a Codable struct.

Ideally this would work like:

// MARK: - Bound
struct Bound: Codable {
    var size: Double? = 3.1415
}

Alternatively I was thinking about how to use new @propertyWrapper to wrap these properties with a default value. However using a @propertyWrapper appears to break the struct's ability to comply with Codable.

Maybe a good topic for a prepitch on evolution?

1 Like

Currently, the only way to do this is to implement init(from:) to assign the default value you need. Codable synthesis is conservative and doesn't fall back to assigned default values to prevent accidental silent data loss/corruption, so you have to be fully explicit if that's what you expect.

I'm don't believe a property wrapper would be enough here, given that decoders throw on missing data (and I don't think a property wrapper would be able to intercept that, since IIRC property wrappers are meant to encode as the underlying value type), but I haven't fully sketched out that possibility.

In the future, having a way to influence Codable synthesis would likely be the most resilient option.

Ok. Thanks for saving me from going down the property wrapper path. Definitely would be nice to see something between full automatic synthesis and having to handle init manually, especially when my real struct is pretty massive.

If I could override just the values I care about and pass the reset onto auto synthesis that'd be nice too (maybe I can and I've missed that).

But what I've proposed above seems the most logical to me in terms of making synthesis behave like you think it would (values that are present are used but overwritten by those found in the serialized data). I guess it'd be different with let values so maybe my idea isn't the best.

Don’t know since when it’s possible, but it is possible :slight_smile:

@propertyWrapper struct DefaultValueIfDecodingFails<Wrapped: Codable & Sendable & DefaultProviding>: Codable, Sendable {
    var wrappedValue: Wrapped

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

    init(from decoder: Decoder) throws {
        do {
            let container = try decoder.singleValueContainer()
            wrappedValue = try container.decode(Wrapped.self)
        } catch {
            wrappedValue = Wrapped.defaultValue
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

// MARK: - DefaultProviding Protocol

/// Protocol to provide default values for types
protocol DefaultProviding {
    static var defaultValue: Self { get }
}

// MARK: - Default implementations for common types

extension String: DefaultProviding {
    static var defaultValue: Self { "" }
}

extension Int: DefaultProviding {
    static var defaultValue: Self { 0 }
}

extension Double: DefaultProviding {
    static var defaultValue: Self { 0.0 }
}

extension Bool: DefaultProviding {
    static var defaultValue: Self { false }
}

extension Array: DefaultProviding where Element: Codable {
    static var defaultValue: Self { [] }
}

extension Optional: DefaultProviding {
    static var defaultValue: Self { .none }
}

// MARK: - Hashable, Equatable

extension DefaultValueIfDecodingFails: Hashable, Equatable where Wrapped: Hashable & Equatable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(wrappedValue)
    }
}

// MARK: - KeyedDecodingContainer Extensions

extension KeyedDecodingContainer {
    /// Decodes a missing key using the type's default value
    func decode<T>(_ type: DefaultValueIfDecodingFails<T>.Type, forKey key: Key) throws -> DefaultValueIfDecodingFails<T> {
        try decodeIfPresent(type, forKey: key) ?? DefaultValueIfDecodingFails<T>(wrappedValue: T.defaultValue)
    }
}

The magic with converting not existing keys to the default value happens in the extension of KeyedDecodingContainer.

With custom types, like enums, you can implement DefaultProviding and also use this property wrapper

I don't see how that helps with the original example:

struct Bound: Codable {
    var size: Double? = 3.1415
}

This might (pseudocode):

  1. encode Bound() to JSON with JSONEncoder
  2. decode that JSON with JSONSerialization into a dictionary
  3. decode a given JSON with JSONSerialization into another dictionary
  4. merge missing keys from 2 into 3
  5. encode the result back into JSON with JSONSerialization
  6. finally decode the resulting JSON into Bound with JSONDecoder

So yeah, doable but convoluted.

Seriously ad-hoc test code for the above:

struct Bound: Codable {
    var size: Double? = 3.1415
    var field = 1
}

let bound = Bound(size: nil, field: 42)
let data = try! JSONEncoder().encode(bound)

var object = try! JSONSerialization.jsonObject(with: data)
let defaultData = try! JSONEncoder().encode(Bound())
let defaultObject = try! JSONSerialization.jsonObject(with: defaultData)

// TODO: generalize this part:
var objectCopy = object as! [String: Any]
let defaultObjectCopy = defaultObject as! [String: Any]
if objectCopy["size"] == nil {
    objectCopy["size"] = defaultObjectCopy["size"]
}
object = objectCopy

let completeData = try! JSONSerialization.data(withJSONObject: object)
let completeObject = try! JSONDecoder().decode(Bound.self, from: completeData)
print(completeObject) // Bound(size: Optional(3.1415), field: 42)

If to wrap into an API I'd say:

JSONDecoder().decode(from: completeData, default: Bound())

in this form you'd be able to supply a different default value appropriate for the occasion.

JSONDecoder().decode(from: completeData, default: Bound(size: 12345))

You’re right. With the original example it’s not possible, except for setting a static default value for all Double’s.

But it’s handy for cases like when you expect an array and instead of an error you want just an empty array. Or empty string. Or a default enum case.