Skip to content

Instantly share code, notes, and snippets.

@AliSoftware
Last active October 31, 2023 12:25
Show Gist options
  • Star 85 You must be signed in to star a gist
  • Fork 19 You must be signed in to fork a gist
  • Save AliSoftware/89b275d7259d23ebf12d377b6ffe15cd to your computer and use it in GitHub Desktop.
Save AliSoftware/89b275d7259d23ebf12d377b6ffe15cd to your computer and use it in GitHub Desktop.
NestableCodingKey: Nice way to define nested coding keys for properties
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)]
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: "/")
}
}
@Frizlab
Copy link

Frizlab commented Feb 11, 2020

That is awesome! Just a quick style comment if I may, I think Data(str.utf8) is more elegant than str.data(using: .utf8)! because it avoids the force unwrap.
Otherwise I’ll probably use this truck in a project of my own 😊

@sstadelman
Copy link

would you consider creating a repo for this, so it can be wrapped in a community swift package? Would like to consume it in our enterprise open source offering.

@AliSoftware
Copy link
Author

@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?

@marksands
Copy link

@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.

@AliSoftware
Copy link
Author

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment