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)
}
Copy link

ghost commented Jun 16, 2020

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

import Foundation

protocol NestableCodingKey: CodingKey {
    var path: [String] { get }
}

extension NestableCodingKey where Self: RawRepresentable, Self.RawValue == String {
    init?(stringValue: String) {
        self.init(rawValue: stringValue)
    }

    var stringValue: String {
        path.first!
    }

    init?(intValue: Int) {
        fatalError()
    }

    var intValue: Int? {
        nil
    }

    var path: [String] {
        self.rawValue.components(separatedBy: "/")
    }
}

public protocol _NestedDecodable: Decodable {
    associatedtype Value: Decodable
    init(wrappedValue: Value)
}

@propertyWrapper
public struct NestedDecodable<Value: Decodable>: _NestedDecodable {
    public var wrappedValue: Value
    public init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
    }
}

extension NestedDecodable: Equatable where Value: Equatable {}

extension KeyedDecodingContainer {

    struct AnyCodingKey: CodingKey {
        let stringValue: String
        let intValue: Int?
        init(stringValue: String) {
            self.stringValue = stringValue
            self.intValue = nil
        }
        init?(intValue: Int) {
            self.stringValue = "\(intValue)"
            self.intValue = intValue
        }
    }

    public func decode<T: _NestedDecodable>(_: T.Type, forKey key: Key) throws -> T {
        guard let nestedKey = key as? NestableCodingKey else { fatalError() }

        let container = try self.nestedContainer(keyedBy: AnyCodingKey.self, forKey: key)
        let nextKeys = nestedKey.path.dropFirst()
        let containerForNestedKey = try nextKeys.indices.dropLast().reduce(container) { (nestedContainer, keyIdx) in
            return try nestedContainer.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue: nextKeys[keyIdx]))
        }

        let wrappedValue = try containerForNestedKey.decode(T.Value.self, forKey: AnyCodingKey(stringValue: nextKeys.last!))
        return T(wrappedValue: wrappedValue)
    }
}

// - Demo

struct Contact: Decodable, Equatable {
    var id: String
    @NestedDecodable<String>
    var firstname: String
    @NestedDecodable<String>
    var address: String

    enum CodingKeys: String, NestableCodingKey {
        case id="id"
        case address="user/details/address"
        case firstname="user/details/name/first"
    }
}

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)
    print(person.address)
    print(person.firstname)
} catch {
    print(error)
}

@ilyapuchka
Copy link
Author

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.

Copy link

ghost commented Jun 16, 2020

awesome, would you consider creating a repo for this?

@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