Skip to content

Instantly share code, notes, and snippets.

@plam4u
Forked from IanKeen/CustomKeyCodable.swift
Created February 25, 2021 16:24
Show Gist options
  • Save plam4u/9640e3346e47405105b38b5822463f54 to your computer and use it in GitHub Desktop.
Save plam4u/9640e3346e47405105b38b5822463f54 to your computer and use it in GitHub Desktop.
PropertyWrapper: CustomKeyCodable allows defining the keys for decoding _per property_
protocol CustomKeyCodable: Codable {
init()
}
extension CustomKeyCodable {
init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: AnyCodingKey.self)
let mirror = Mirror(reflecting: self)
for (label, value) in mirror.children {
guard let label = label else {
throw DecodingError.keyNotFound(AnyCodingKey(""), .init(codingPath: container.codingPath, debugDescription: "Not an KeyedContainer"))
}
guard let key = value as? KeyType else {
throw DecodingError.keyNotFound(AnyCodingKey(label), .init(codingPath: container.codingPath, debugDescription: "All keys must use @Key property wrapper"))
}
try key.decode(container: container, key: key.name ?? String(label.dropFirst()))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyCodingKey.self)
let mirror = Mirror(reflecting: self)
for (label, value) in mirror.children {
guard let label = label else {
throw EncodingError.invalidValue(self, .init(codingPath: container.codingPath, debugDescription: "Not an KeyedContainer"))
}
guard let key = value as? KeyType else {
throw EncodingError.invalidValue(value, .init(codingPath: container.codingPath, debugDescription: "All keys must use @Key property wrapper"))
}
try key.encode(container: &container, key: key.name ?? String(label.dropFirst()))
}
}
}
private protocol KeyType {
var name: String? { get }
func decode(container: KeyedDecodingContainer<AnyCodingKey>, key: String) throws
func encode(container: inout KeyedEncodingContainer<AnyCodingKey>, key: String) throws
}
extension Key: KeyType {}
@propertyWrapper
public final class Key<T: Codable>: Codable {
public var name: String?
public var wrappedValue: T {
get {
guard let value = value else { fatalError("Unable to access value before it is set") }
return value
}
set { value = newValue }
}
private var value: T?
public init() { }
public init(_ name: String) {
self.name = name
}
public init(wrappedValue: T) {
self.value = wrappedValue
}
public init(wrappedValue: T, _ name: String) {
self.name = name
self.value = wrappedValue
}
func decode(container: KeyedDecodingContainer<AnyCodingKey>, key: String) throws {
self.name = key
self.value = try container.decode(T.self, forKey: AnyCodingKey(key))
}
func encode(container: inout KeyedEncodingContainer<AnyCodingKey>, key: String) throws {
try container.encode(value, forKey: AnyCodingKey(key))
}
}
// Note: All properties must be marked with `@Key`
// provide a value if you are specifying a key, otherwise you can omit it
struct User: CustomKeyCodable {
@Key("first_name") var firstName: String
@Key("last_name") var lastName: String
@Key var age: Int
init() { }
}
let json = Data(#"{"first_name":"Ian", "last_name": "Keen", "age":42}"#.utf8)
var model = try! JSONDecoder().decode(User.self, from: json)
print(model.firstName) // Ian
print(model.lastName) // Keen
print(model.age) // 42
try print(String(data: JSONEncoder().encode(model), encoding: .utf8)!)
//{"age":42,"first_name":"Ian","last_name":"Keen"}
model.lastName = "Smith"
model.age = 123
try print(String(data: JSONEncoder().encode(model), encoding: .utf8)!)
//{"age":123,"first_name":"Ian","last_name":"Smith"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment