The future of serialization & deserialization APIs

I'll add a vote for something like @CodingDefault(.unknown). :hand_with_fingers_splayed:

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.

3 Likes

Thank you so much for working on this

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

1 Like

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)
1 Like