Skip to content

Instantly share code, notes, and snippets.

@ilyapuchka
Last active June 6, 2022 15:00
Show Gist options
  • Save ilyapuchka/52356678ca87b1303a161cecdcf1a240 to your computer and use it in GitHub Desktop.
Save ilyapuchka/52356678ca87b1303a161cecdcf1a240 to your computer and use it in GitHub Desktop.
Decoding nested values with property wrapper https://twitter.com/ilyapuchka/status/1226861244398915585
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)
}
@ilyapuchka
Copy link
Author

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