CodableWrappers
CodableWrappers copied to clipboard
Default values when key is not present
Hi, I've been working on a similar set of features as this project, until I found this one, great work!
The one feature I'm missing is for a way to have a non optional field that contains a default value in case the key is not present in the input data. So far I was able to get to this solution, which works for my use case, and was wondering if it might be something that could be added to this project
What I came up with was this:
import Foundation
public protocol DefaultValue<Value> where Value: Codable {
associatedtype Value
static var defaultValue: Value { get }
}
@propertyWrapper
public struct DefaultCodable<D: DefaultValue>: Codable {
private enum State<Value> {
case uninitialized
case initialized(Value)
}
private var innerValue: State<D.Value>
public init(value: D.Value? = nil) {
if let value {
innerValue = .initialized(value)
} else {
innerValue = .uninitialized
}
}
public var wrappedValue: D.Value {
get {
if case .initialized(let value) = innerValue {
return value
} else {
return D.self.defaultValue
}
}
set {
innerValue = .initialized(newValue)
}
}
fileprivate var isSet: Bool {
if case .uninitialized = innerValue {
return false
} else {
return true
}
}
public func encode(to encoder: Encoder) throws {
if case .initialized(let value) = innerValue {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.innerValue = try .initialized(container.decode(D.Value.self))
}
}
extension KeyedEncodingContainer {
public mutating func encode<T>(
_ value: DefaultCodable<T>,
forKey key: KeyedEncodingContainer<K>.Key
) throws {
if value.isSet {
try encode(value.wrappedValue, forKey: key)
}
}
}
extension KeyedDecodingContainer {
public func decode<T>(
_ type: DefaultCodable<T>.Type,
forKey key: Self.Key
) throws -> DefaultCodable<T> {
if contains(key) {
let value = try decode(T.Value.self, forKey: key)
return DefaultCodable(value: value)
} else {
return DefaultCodable()
}
}
}
// Default Values
public enum DefaultBoolTrue: DefaultValue {
public static var defaultValue = true
}
public enum DefaultBoolFalse: DefaultValue {
public static var defaultValue = false
}
which can be used like this:
public enum DefaultNameUnknown: DefaultValue {
public static var emptyDefault = "Unknown"
}
public struct UserInfo: Codable {
@DefaultCodable<DefaultBoolFalse> public var admin: Bool
@DefaultCodable<DefaultBoolTrue> public var isActive: Bool
@DefaultCodable<DefaultNameUnknown> public var name: String
public init() {}
}
And used like this:
public enum DefaultNameUnknown: DefaultValue {
public static var defaultValue = "Unknown"
}
public struct UserInfo: Codable {
@DefaultCodable<DefaultBoolFalse> public var admin: Bool
@DefaultCodable<DefaultBoolTrue> public var isActive: Bool
@DefaultCodable<DefaultNameUnknown> public var name: String
public init() {}
}
which results in
let data = #"{}"#.data(using: .utf8)!
let decoder = JSONDecoder()
let user = try decoder.decode(UserInfo.self, from: data)
print(user.admin) // false
print(user.isActive) // true
print(user.name) // Unknown
This is using 5.7 syntax, but could probably be adapted. Also, there might be a better way to specify a default value other than types, but this worked for me well enough.
Is this something that you might consider adding to CodableWrappers?
Thank you! Did you try FallbackValueProvider?
Yeah, just did, the downside of this is that it requires the property to be an optional, do you know why? If there's a default value, shouldn't it be non optional?
@sergiocampama I think it's to support encoding specifically so you can omit properties on construction.
@FallbackDecoding<EmptyString>
var name: String
@FallbackEncoding<EmptyString>
var shortName: String?
// The setup up above makes this possible
let product = Product(name: "xxx")
// If FallbackEncoding or FallbackCodable becomes non optional
@FallbackEncoding<EmptyString>
var shortName: String
Then you'll be forced to do this which goes against having the default value.
let product = Product(name: "xxx", shortName: "xxxx")
that's only a convenience if you're using it in the same module, if you are exporting it you still need to create an initializer for it, where you can choose to take an optional value there and only set it when present
Yeah, just did, the downside of this is that it requires the property to be an optional, do you know why? If there's a default value, shouldn't it be non optional?
The intention of the library is to handle serialization, but I can understand wanting things in both directions. That's probably something you'll have to handle yourself in v2.0. Working on a Macro solution (CodingKeys are done) and I think it'll be easier to handle that kind of thing with that solution.
Closing for now but will keep your use case in mind for the Macros version :)