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 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 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: "/") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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.