Last active
October 31, 2023 12:25
-
-
Save AliSoftware/89b275d7259d23ebf12d377b6ffe15cd to your computer and use it in GitHub Desktop.
NestableCodingKey: Nice way to define nested coding keys for properties
This file contains hidden or 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
struct Contact: Decodable, CustomStringConvertible { | |
var id: String | |
@NestedKey | |
var firstname: String | |
@NestedKey | |
var lastname: String | |
@NestedKey | |
var address: String | |
enum CodingKeys: String, NestableCodingKey { | |
case id | |
case firstname = "nested/data/user/firstname" | |
case lastname = "nested/data/user/lastname" | |
case address = "nested/data/address" | |
} | |
var description: String { | |
"Contact(firstname: \(firstname), lastname: \(lastname), address: \(address))" | |
} | |
} | |
let json = """ | |
[ | |
{ | |
"id": "1", | |
"nested": { "data": { | |
"user": { "firstname": "Alice", "lastname": "Wonderland" }, | |
"address": "Through the looking glass" | |
} } | |
}, | |
{ | |
"id": "2", | |
"nested": { "data": { | |
"user": { "firstname": "Bob", "lastname": "Builder" }, | |
"address": "1, NewRoad" | |
} } | |
} | |
] | |
""".data(using: .utf8)! | |
let decoder = JSONDecoder() | |
let list = try decoder.decode([Contact].self, from: json) | |
// [Contact(firstname: Alice, lastname: Wonderland, address: Through the looking glass), Contact(firstname: Bob, lastname: Builder, address: 1, NewRoad)] | |
This file contains hidden or 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 | |
//: # NestedKey | |
/// | |
/// Use this to annotate the properties that require a depth traversal during decoding. | |
/// The corresponding `CodingKey` for this property must be a `NestableCodingKey` | |
@propertyWrapper | |
struct NestedKey<T: Decodable>: Decodable { | |
var wrappedValue: T | |
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 | |
} | |
} | |
init(from decoder: Decoder) throws { | |
let key = decoder.codingPath.last! | |
guard let nestedKey = key as? NestableCodingKey else { | |
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Key \(key) is not a NestableCodingKey")) | |
} | |
let nextKeys = nestedKey.path.dropFirst() | |
// key descent | |
let container = try decoder.container(keyedBy: AnyCodingKey.self) | |
let lastLeaf = try nextKeys.indices.dropLast().reduce(container) { (nestedContainer, keyIdx) in | |
do { | |
return try nestedContainer.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue: nextKeys[keyIdx])) | |
} catch DecodingError.keyNotFound(let key, let ctx) { | |
try NestedKey.keyNotFound(key: key, ctx: ctx, container: container, nextKeys: nextKeys[..<keyIdx]) | |
} | |
} | |
// key leaf | |
do { | |
self.wrappedValue = try lastLeaf.decode(T.self, forKey: AnyCodingKey(stringValue: nextKeys.last!)) | |
} catch DecodingError.keyNotFound(let key, let ctx) { | |
try NestedKey.keyNotFound(key: key, ctx: ctx, container: container, nextKeys: nextKeys.dropLast()) | |
} | |
} | |
private static func keyNotFound<C: Collection>( | |
key: CodingKey, ctx: DecodingError.Context, | |
container: KeyedDecodingContainer<AnyCodingKey>, nextKeys: C) throws -> Never | |
where C.Element == String | |
{ | |
throw DecodingError.keyNotFound(key, DecodingError.Context( | |
codingPath: container.codingPath + nextKeys.map(AnyCodingKey.init(stringValue:)), | |
debugDescription: "NestedKey: No value associated with key \"\(key.stringValue)\"", | |
underlyingError: ctx.underlyingError | |
)) | |
} | |
} | |
//: # NestableCodingKey | |
/// Use this instead of `CodingKey` to annotate your `enum CodingKeys: String, NestableCodingKey`. | |
/// Use a `/` to separate the components of the path to nested keys | |
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: "/") | |
} | |
} |
@AliSoftware Absolutely! I was following along last night and didn't want to steal your thunder. I've been trying to crib your idea to cook up some other nice-to-haves, so I'll see what comes out of it.
👍
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@sstadelman would be nice, but for now this is just a gist I hacked in one evening, not battle tested. To make it into a proper OSS package that I'd be confident sharing it would first require some tests; and would require some maintenance time that I don't have anymore (I mean, I already have quite a lot of work waiting in my backlog to get back on SwiftGen an other existing repos, and making a package kind of entitles me to maintain it but if I don't have time better not make promise that official)
I'd love this idea to become part of an existing lib though which have maintainers and already a setup for unit tests and all. @marksands interested in including the idea behind that gist into BetterCodable?