I'll add a vote for something like @CodingDefault(.unknown).
Missing keys (and setting a default value) may be the single most common reason for needing to implement codable functions in network data structures in projects I've worked on.
The future-proofing case (encountering an unexpected key) is also important, but I would rank it less important than handling missing keys.
Another +1 for an architecture that can play nicely with more than one serialization format. When writing data exporters and importers exposed to users, it would be handy to have something that would make it easy to swap serialization methods at run time.
I would also +1 having a builder as part of this. I'm picturing a Serializable/SerializationRepresentation along the lines of a Transferable/TransferRepresentation?
Going back to the visitor example in the OP, it's possible to implement this as an extension to the existing interface with Decoder implementations providing their own if they wish. I've included the complete implementation in a detail below the example.
Note that the example relies on the value being explicitly passed along with the key (via SingleValueDecodingContainer) rather than relying on the decoding call implicitly tied to the current key.
extension Decoder {
public func keyedSequence<Key>(keyedBy type: Key.Type) throws -> any Sequence<(Key, any SingleValueDecodingContainer)> where Key : CodingKey
}
struct Person: Decodable {
let name: String
let age: Int
init(from decoder: any Decoder) throws {
var name: String?
var age: Int?
for (key, container) in try decoder.keyedSequence(keyedBy: CodingKeys.self) {
switch key {
case .name: name = try container.decode(String.self)
case .age: age = try container.decode(Int.self)
}
}
guard let name else { throw ValueNotSetError() }
guard let age else { throw ValueNotSetError() }
self.name = name
self.age = age
}
}
Complete Implementation
import Foundation
extension Decoder {
public func keyedSequence<Key>(keyedBy type: Key.Type) throws -> any Sequence<(Key, any SingleValueDecodingContainer)> where Key : CodingKey {
IteratorSequence(KeyedSingleValueDecodingIterator(keyedContainer: try container(keyedBy: Key.self)))
}
}
struct KeyedSingleValueDecodingIterator<Key>: IteratorProtocol where Key: CodingKey {
mutating func next() -> (Key, any SingleValueDecodingContainer)? {
guard let key = keyIterator.next() else { return nil }
return (key, KeyedSingleValueDecodingContainer(keyedContainer: keyedContainer, key: key))
}
var keyIterator: IndexingIterator<[Key]>
var keyedContainer: KeyedDecodingContainer<Key>
init(keyedContainer: KeyedDecodingContainer<Key>) {
self.keyIterator = keyedContainer.allKeys.makeIterator()
self.keyedContainer = keyedContainer
}
}
struct KeyedSingleValueDecodingContainer<Key>: SingleValueDecodingContainer where Key : CodingKey {
var codingPath: [any CodingKey] {
keyedContainer.codingPath + [key]
}
var keyedContainer: KeyedDecodingContainer<Key>
let key: Key
func decodeNil() -> Bool {
try! keyedContainer.decodeNil(forKey: key)
}
func decode(_ type: Bool.Type) throws -> Bool {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: String.Type) throws -> String {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: Double.Type) throws -> Double {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: Float.Type) throws -> Float {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: Int.Type) throws -> Int {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: Int8.Type) throws -> Int8 {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: Int16.Type) throws -> Int16 {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: Int32.Type) throws -> Int32 {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: Int64.Type) throws -> Int64 {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: UInt.Type) throws -> UInt {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: UInt8.Type) throws -> UInt8 {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: UInt16.Type) throws -> UInt16 {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: UInt32.Type) throws -> UInt32 {
try keyedContainer.decode(type, forKey: key)
}
func decode(_ type: UInt64.Type) throws -> UInt64 {
try keyedContainer.decode(type, forKey: key)
}
func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
try keyedContainer.decode(type, forKey: key)
}
}
struct ValueNotSetError: Error { }
struct Person: Decodable {
let name: String
let age: Int
enum CodingKeys: CodingKey {
case name
case age
}
init(from decoder: any Decoder) throws {
var name: String?
var age: Int?
for (key, container) in try decoder.keyedSequence(keyedBy: CodingKeys.self) {
switch key {
case .name: name = try container.decode(String.self)
case .age: age = try container.decode(Int.self)
}
}
guard let name else { throw ValueNotSetError() }
guard let age else { throw ValueNotSetError() }
self.name = name
self.age = age
}
}
let json = """
{
"name": "Martha",
"age": 6
}
"""
let data = Data(json.utf8)
let person = try JSONDecoder().decode(Person.self, from: data)
print(person)