CodableWrappers icon indicating copy to clipboard operation
CodableWrappers copied to clipboard

Default values when key is not present

Open sergiocampama opened this issue 3 years ago • 4 comments

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?

sergiocampama avatar Jul 31 '22 14:07 sergiocampama

Thank you! Did you try FallbackValueProvider?

GottaGetSwifty avatar Aug 07 '22 00:08 GottaGetSwifty

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 avatar Aug 08 '22 01:08 sergiocampama

@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")

steffidg avatar Aug 15 '22 05:08 steffidg

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

sergiocampama avatar Aug 15 '22 21:08 sergiocampama

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 :)

GottaGetSwifty avatar Sep 13 '23 16:09 GottaGetSwifty