Last active
June 6, 2022 15:00
-
-
Save ilyapuchka/52356678ca87b1303a161cecdcf1a240 to your computer and use it in GitHub Desktop.
Decoding nested values with property wrapper https://twitter.com/ilyapuchka/status/1226861244398915585
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public struct Unit: Codable, Equatable { | |
init() {} | |
public init(from decoder: Decoder) {} | |
public func encode(to encoder: Encoder) throws {} | |
public static func ==(lhs: Self, rhs: Self) -> Bool { | |
return true | |
} | |
} | |
public protocol _NestedDecodable: Decodable { | |
associatedtype Value: Decodable | |
associatedtype NestedKeys: CodingKey & CaseIterable | |
init(wrappedValue: Value) | |
} | |
@propertyWrapper | |
public struct NestedDecodable<Value: Decodable, NestedKeys: CodingKey & CaseIterable>: _NestedDecodable { | |
public var wrappedValue: Value | |
public init(wrappedValue: Value) { | |
self.wrappedValue = wrappedValue | |
} | |
} | |
public protocol _NestedEncodable: Encodable { | |
associatedtype Value: Encodable | |
associatedtype NestedKeys: CodingKey & CaseIterable | |
var wrappedValue: Value { get } | |
} | |
@propertyWrapper | |
public struct NestedEncodable<Value: Encodable, NestedKeys: CodingKey & CaseIterable>: _NestedEncodable { | |
public var wrappedValue: Value | |
public init(wrappedValue: Value) { | |
self.wrappedValue = wrappedValue | |
} | |
} | |
@propertyWrapper | |
public struct NestedCodable<Value: Codable, NestedKeys: CodingKey & CaseIterable>: _NestedDecodable, _NestedEncodable { | |
public var wrappedValue: Value | |
public init(wrappedValue: Value) { | |
self.wrappedValue = wrappedValue | |
} | |
} | |
extension NestedDecodable: Equatable where Value: Equatable {} | |
extension NestedEncodable: Equatable where Value: Equatable {} | |
extension NestedCodable: Equatable where Value: Equatable {} | |
extension KeyedDecodingContainer { | |
public func decode(_: Unit.Type, forKey key: Key) throws -> Unit { Unit() } | |
public func decode<T: _NestedDecodable>(_: T.Type, forKey key: Key) throws -> T { | |
guard T.NestedKeys.allCases.isEmpty == false else { | |
throw DecodingError.valueNotFound(T.NestedKeys.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No keys defined in \(K.self)")) | |
} | |
let wrappedValue = try containerForNestedKey().decode(T.Value.self, forKey: T.NestedKeys.lastCase!) | |
return T(wrappedValue: wrappedValue) | |
} | |
public func decode<T: _NestedDecodable, Value>(_: T.Type, forKey key: Key) throws -> T where T.Value == Value? { | |
guard T.NestedKeys.allCases.isEmpty == false else { | |
throw DecodingError.valueNotFound(T.NestedKeys.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No keys defined in \(K.self)")) | |
} | |
let wrappedValue = try containerForNestedKey().decodeIfPresent(Value.self, forKey: T.NestedKeys.lastCase!) | |
return T(wrappedValue: wrappedValue) | |
} | |
private func containerForNestedKey<K: CodingKey & CaseIterable>() throws -> KeyedDecodingContainer<K> { | |
guard let rootKey = Key(stringValue: K.allCases.first!.stringValue) else { | |
throw DecodingError.valueNotFound(Key.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No root key in \(Key.self) with string value `\(K.allCases.first!.stringValue)`")) | |
} | |
var container = try self.nestedContainer(keyedBy: K.self, forKey: rootKey) | |
if K.allCases.count > 1 { | |
try K.allCases.dropFirst().dropLast().forEach { (key) in | |
container = try container.nestedContainer(keyedBy: K.self, forKey: key) | |
} | |
} | |
return container | |
} | |
} | |
extension KeyedEncodingContainer { | |
public mutating func encode(_ value: Unit, forKey key: Key) throws {} | |
public mutating func encode<T: _NestedEncodable>(_ value: T, forKey key: Key) throws { | |
guard T.NestedKeys.allCases.isEmpty == false else { | |
throw DecodingError.valueNotFound(T.NestedKeys.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No keys defined in \(K.self)")) | |
} | |
var container: KeyedEncodingContainer<T.NestedKeys> = try containerForNestedKey() | |
try container.encode(value.wrappedValue, forKey: T.NestedKeys.lastCase!) | |
} | |
public mutating func encode<T: _NestedEncodable, Value>(_ value: T, forKey key: Key) throws where T.Value == Value? { | |
guard T.NestedKeys.allCases.isEmpty == false else { | |
throw DecodingError.valueNotFound(T.NestedKeys.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No keys defined in \(K.self)")) | |
} | |
var container: KeyedEncodingContainer<T.NestedKeys> = try containerForNestedKey() | |
try container.encodeIfPresent(value.wrappedValue, forKey: T.NestedKeys.lastCase!) | |
} | |
private mutating func containerForNestedKey<K: CodingKey & CaseIterable>() throws -> KeyedEncodingContainer<K> { | |
guard let rootKey = Key(stringValue: K.allCases.first!.stringValue) else { | |
throw DecodingError.valueNotFound(Key.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No root key in \(Key.self) with string value `\(K.allCases.first!.stringValue)`")) | |
} | |
var container = self.nestedContainer(keyedBy: K.self, forKey: rootKey) | |
if K.allCases.count > 1 { | |
K.allCases.dropFirst().dropLast().forEach { (key) in | |
container = container.nestedContainer(keyedBy: K.self, forKey: key) | |
} | |
} | |
return container | |
} | |
} | |
extension CodingKey where Self: CaseIterable { | |
static var lastCase: Self? { | |
guard allCases.isEmpty == false else { return nil } | |
let lastIndex = allCases.index(allCases.endIndex, offsetBy: -1) | |
return allCases[lastIndex] | |
} | |
} | |
struct Contact: Codable, Equatable { | |
var id: String | |
@NestedCodable<String, FirstNameKeys> | |
var firstname: String | |
@NestedCodable<String?, LastNameKeys> | |
var lastname: String? | |
@NestedCodable<String, AddressKeys> | |
var address: String | |
// here just for compiler, does not decode anything | |
private var user: Unit | |
enum FirstNameKeys: String, CodingKey, CaseIterable { | |
case user, details, name, first | |
} | |
enum LastNameKeys: String, CodingKey, CaseIterable { | |
case user, details, name, last | |
} | |
enum AddressKeys: String, CodingKey, CaseIterable { | |
case user, details, address | |
} | |
} | |
let data = """ | |
{ | |
"id": "1", | |
"user": { | |
"details": { | |
"address": "Apple St.", | |
"name": { | |
"first": "Swift", | |
"last": "Language" | |
} | |
} | |
} | |
} | |
""".data(using: .utf8)! | |
do { | |
let person = try JSONDecoder().decode(Contact.self, from: data) | |
print(person) | |
let data1 = try JSONEncoder().encode(person) | |
print(String(data: data1, encoding: .utf8)!) | |
} catch { | |
print(error) | |
} |
That's one of the ways. But it requires to define coding keys for all the properties, not just nested. Whatever works best though, there is no single right solution.
awesome, would you consider creating a repo for this?
Nah, this was never my intention to make it a library. Code snippet is enough. But I'll refer an original Twitter discussion where both approaches were mentioned.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
rather than having enum for each nested key how about creating a custom nestable coding key! that holds the full path to the nested key