Codable requirements mystery

I'm learning how to make a custom encoder, this time following the "fatalError driven approach" – implementing all required protocol conformances with stub methods that have "fatalError()" in them, making the code fully compilable, then running the code, and fixing each fatalError() that is being hit on the "fix as you go" basis. (FWIW I used this technique on multiple occasions in the past and can highly recommend its usefulness for learning purposes).

Here's what I have so far:


The code
import Combine // for TopLevelEncoder

let encoder = MyEncoder()
let topLevelEncoder = MyTopLevelEncoder()

struct MyKeyedEncodingContainer<Key: CodingKey> : KeyedEncodingContainerProtocol {
    
    mutating func encodeNil(forKey key: Key) throws {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func encode(_ value: Bool, forKey key: Key) throws {
        print("MyKeyedEncodingContainer.encode Bool: \(value) forKey: \(key.stringValue)")
    }
    mutating func encode(_ value: String, forKey key: Key) throws {
        print("MyKeyedEncodingContainer.encode String: \(value) forKey: \(key.stringValue)")
    }
    mutating func encode(_ value: Int, forKey key: Key) throws {
        print("MyKeyedEncodingContainer.encode Int: \(value) forKey: \(key.stringValue)")
    }
    // ditto for double, float, int8, int16, etc
    
    mutating func encodeConditional<T>(_ object: T, forKey key: Self.Key) throws where T : AnyObject, T : Encodable {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    
    mutating func encodeIfPresent(_ value: Bool?, forKey key: Key) throws {
        precondition(value == nil)
        print("MyKeyedEncodingContainer.encode Bool nil forKey: \(key.stringValue)")
    }
    mutating func encodeIfPresent(_ value: String?, forKey key: Key) throws {
        precondition(value == nil)
        print("MyKeyedEncodingContainer.encode String nil forKey: \(key.stringValue)")
    }
    mutating func encodeIfPresent(_ value: Int?, forKey key: Key) throws {
        precondition(value == nil)
        print("MyKeyedEncodingContainer.encode Int nil forKey: \(key.stringValue)")
    }
    // ditto for double, float, int8, int16, etc

    mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
        print("MyKeyedEncodingContainer.encode T forKey: \(key.stringValue)")
        try value.encode(to: encoder)
    }
    
    var codingPath: [CodingKey] { [] } // MARK: TODO
    
    mutating func nestedContainer<NestedKey: CodingKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func superEncoder() -> Encoder {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func superEncoder(forKey key: Key) -> Encoder {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
}

struct MyUnkeyedEncodingContainer: UnkeyedEncodingContainer {
    mutating func encodeNil() throws {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func encode(_ value: Bool) throws {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func encode(_ value: String) throws {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func encode(_ value: Int) throws {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    // ditto for double, float, int8, int16, etc
    
    mutating func encodeConditional<T>(_ object: T) throws where T : AnyObject, T : Encodable {
        fatalError("WHEN IS IT CALLED? 🛑")
    }

    mutating func encode<T: Encodable>(_ value: T) throws {
        print("MyUnkeyedEncodingContainer.encode T")
        try value.encode(to: encoder)
    }
    var codingPath: [CodingKey] { [] } // MARK: TODO
    var count: Int { 0 } // MARK: TODO
    
    mutating func nestedContainer<NestedKey: CodingKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
    mutating func superEncoder() -> Encoder {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
}

struct MySingleValueEncodingContainer: SingleValueEncodingContainer {
    var codingPath: [CodingKey] { [] } // MARK: TODO
    
    mutating func encodeNil() throws {
        print("MySingleValueEncodingContainer.encode nil")
    }
    mutating func encode(_ value: Bool) throws {
        print("MySingleValueEncodingContainer.encode bool: \(value)")
    }
    mutating func encode(_ value: String) throws {
        print("MySingleValueEncodingContainer.encode string: \(value)")
    }
    mutating func encode(_ value: Int) throws {
        print("MySingleValueEncodingContainer.encode Int: \(value)")
    }
    // ditto for double, float, int8, int16, etc
    
    mutating func encode<T: Encodable>(_ value: T) throws {
        fatalError("WHEN IS IT CALLED? 🛑")
    }
}

class MyEncoder: Encoder {
    var codingPath: [CodingKey] { [] } // MARK: TODO
    var userInfo: [CodingUserInfoKey : Any] = [:]
    
    func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
        KeyedEncodingContainer<Key>(MyKeyedEncodingContainer())
    }
    func unkeyedContainer() -> UnkeyedEncodingContainer {
        MyUnkeyedEncodingContainer()
    }
    func singleValueContainer() -> SingleValueEncodingContainer {
        MySingleValueEncodingContainer()
    }
}

struct MyTopLevelEncoder: TopLevelEncoder {
    @discardableResult
    func encode<T: Encodable>(_ value: T) throws -> Data {
        try value.encode(to: encoder)
        return Data() // MARK: TODO
    }
}

func genericEncode<T: Encodable>(_ value: T) {
    try! topLevelEncoder.encode(value)
    try! value.encode(to: encoder)
}

func encodingTest() {
    struct Strct: Encodable {
        let x = 0
    }
    
    class C: Encodable {
        let x = 0
    }
    
    struct S: Encodable {
        let boolNil: Bool? = nil
        let stringNil: String? = nil
        let intNil: Int? = nil
        let arrayNil: [Int]? = nil
        let dictNil: [String: Int]? = nil
        let strctNil: Strct? = nil
        
        let bool: Bool = true
        let string: String = "str"
        let int: Int = 42
        let array = [Strct()]
        let dict = ["key": Strct()]
        let strct = Strct()
    }
    
    let key = "key"
    
    let boolNil = nil as Bool?
    let stringNil = nil as String?
    let intNil = nil as Int?
    let arrayNil = nil as [Int]?
    let dictNil = nil as [String:Int]?
    let strctNil = nil as Strct?
    let clssNil = nil as C?

    let bool = true
    let string = "string"
    let int = 42
    let array = [Strct()]
    let dict = [key: Strct()]
    let strct = Strct()
    let clss = C()

    // MARK: -----------------------------------------------------
    // MARK: topLevelEncoder test
    
    // MARK: topLevelEncoder: nil
    try! topLevelEncoder.encode(boolNil)            //
    try! topLevelEncoder.encode(stringNil)          //
    try! topLevelEncoder.encode(intNil)             //
    try! topLevelEncoder.encode(arrayNil)           //
    try! topLevelEncoder.encode(dictNil)            //
    try! topLevelEncoder.encode(strctNil)           //
    try! topLevelEncoder.encode(clssNil)            //
    print()

    // MARK: topLevelEncoder: value
    try! topLevelEncoder.encode(bool)               //
    try! topLevelEncoder.encode(string)             //
    try! topLevelEncoder.encode(int)                //
    try! topLevelEncoder.encode(array)              //
    try! topLevelEncoder.encode(dict)               //
    try! topLevelEncoder.encode(strct)              //
    try! topLevelEncoder.encode(clss)               //
    print()
    
    // MARK: topLevelEncoder: [nil]
    try! topLevelEncoder.encode([boolNil])          //
    try! topLevelEncoder.encode([stringNil])        //
    try! topLevelEncoder.encode([intNil])           //
    try! topLevelEncoder.encode([arrayNil])         //
    try! topLevelEncoder.encode([dictNil])          //
    try! topLevelEncoder.encode([strctNil])         //
    try! topLevelEncoder.encode([clss])             //
    print()

    // MARK: topLevelEncoder: [value]
    try! topLevelEncoder.encode([bool])             //
    try! topLevelEncoder.encode([string])           //
    try! topLevelEncoder.encode([int])              //
    try! topLevelEncoder.encode([array])            //
    try! topLevelEncoder.encode([dict])             //
    try! topLevelEncoder.encode([strct])            //
    try! topLevelEncoder.encode([clss])             //
    print()
    
    // MARK: topLevelEncoder: [key: nil]
    try! topLevelEncoder.encode([key: boolNil])
    try! topLevelEncoder.encode([key: stringNil])   //
    try! topLevelEncoder.encode([key: intNil])      //
    try! topLevelEncoder.encode([key: arrayNil])    //
    try! topLevelEncoder.encode([key: dictNil])     //
    try! topLevelEncoder.encode([key: strctNil])    //
    try! topLevelEncoder.encode([key: clssNil])     //
    print()

    // MARK: topLevelEncoder: [key: value]
    try! topLevelEncoder.encode([key: bool])        //
    try! topLevelEncoder.encode([key: string])      //
    try! topLevelEncoder.encode([key: int])         //
    try! topLevelEncoder.encode([key: array])       //
    try! topLevelEncoder.encode([key: dict])        //
    try! topLevelEncoder.encode([key: strct])       //
    try! topLevelEncoder.encode([key: clss])        //
    print()
    
    // MARK: -----------------------------------------------------
    // MARK: (non top level) encoder test
    
    // MARK: (non top level) encoder: nil
    try! boolNil.encode(to: encoder)
    try! stringNil.encode(to: encoder)
    try! intNil.encode(to: encoder)
    try! arrayNil.encode(to: encoder)
    try! dictNil.encode(to: encoder)
    try! strctNil.encode(to: encoder)
    try! clssNil.encode(to: encoder)
    print()

    // MARK: (non top level) encoder: value
    try! bool.encode(to: encoder)
    try! string.encode(to: encoder)
    try! int.encode(to: encoder)
    try! array.encode(to: encoder)
    try! dict.encode(to: encoder)
    try! strct.encode(to: encoder)
    try! clss.encode(to: encoder)
    print()
    
    // MARK: (non top level) encoder: [nil]
    try! [boolNil].encode(to: encoder)
    try! [stringNil].encode(to: encoder)
    try! [intNil].encode(to: encoder)
    try! [arrayNil].encode(to: encoder)
    try! [dictNil].encode(to: encoder)
    try! [strctNil].encode(to: encoder)
    try! [clss].encode(to: encoder)
    print()

    // MARK: (non top level) encoder: [value]
    try! [bool].encode(to: encoder)
    try! [string].encode(to: encoder)
    try! [int].encode(to: encoder)
    try! [array].encode(to: encoder)
    try! [dict].encode(to: encoder)
    try! [strct].encode(to: encoder)
    try! [clss].encode(to: encoder)
    print()
    
    // MARK: (non top level) encoder: [key: nil]
    try! [key: boolNil].encode(to: encoder)
    try! [key: stringNil].encode(to: encoder)
    try! [key: intNil].encode(to: encoder)
    try! [key: arrayNil].encode(to: encoder)
    try! [key: dictNil].encode(to: encoder)
    try! [key: strctNil].encode(to: encoder)
    try! [key: clssNil].encode(to: encoder)
    print()

    // MARK: (non top level) encoder: [key: value]
    try! [key: bool].encode(to: encoder)
    try! [key: string].encode(to: encoder)
    try! [key: int].encode(to: encoder)
    try! [key: array].encode(to: encoder)
    try! [key: dict].encode(to: encoder)
    try! [key: strct].encode(to: encoder)
    try! [key: clss].encode(to: encoder)
    print()
    
    // MARK: -----------------------------------------------------
    // MARK: generic encoding test
    
    // MARK: generic encoding: nil
    genericEncode(boolNil)
    genericEncode(stringNil)
    genericEncode(intNil)
    genericEncode(arrayNil)
    genericEncode(dictNil)
    genericEncode(strctNil)
    genericEncode(clssNil)
    print()

    // MARK: generic encoding: value
    genericEncode(bool)
    genericEncode(string)
    genericEncode(int)
    genericEncode(array)
    genericEncode(dict)
    genericEncode(strct)
    genericEncode(clss)
    print()
    
    // MARK: generic encoding: [nil]
    genericEncode([boolNil])
    genericEncode([stringNil])
    genericEncode([intNil])
    genericEncode([arrayNil])
    genericEncode([dictNil])
    genericEncode([strctNil])
    genericEncode([clss])
    print()

    // MARK: generic encoding: [value]
    genericEncode([bool])
    genericEncode([string])
    genericEncode([int])
    genericEncode([array])
    genericEncode([dict])
    genericEncode([strct])
    genericEncode([clss])
    print()
    
    // MARK: generic encoding: [key: nil]
    genericEncode([key: boolNil])
    genericEncode([key: stringNil])
    genericEncode([key: intNil])
    genericEncode([key: arrayNil])
    genericEncode([key: dictNil])
    genericEncode([key: strctNil])
    genericEncode([key: clssNil])
    print()

    // MARK: generic encoding: [key: value]
    genericEncode([key: bool])
    genericEncode([key: string])
    genericEncode([key: int])
    genericEncode([key: array])
    genericEncode([key: dict])
    genericEncode([key: strct])
    genericEncode([key: clss])
    print()

    print()
}

encodingTest()

I deliberately limited encoding to these basic types (bool, string, int, array, dictionary, struct, and class) and their derivatives (nil's of various types, arrays and dictionaries of various types). The final code should contain the handling of types like int8, double, etc.

The types in this code are:

  • MyTopLevelEncoder
  • MyEncoder (not top level)
  • MySingleValueEncodingContainer
  • MyUnkeyedEncodingContainer
  • MyKeyedEncodingContainer

The above code has the built-in test which I believe to be quite thorough. In the test I exercise both "top-level" encoder and "non top-level encoder". However I can not hit the entry-points marked with fatalError("WHEN IS IT CALLED? 🛑"). In particular:

  • "nested" container requirements
  • "super" encoder requirements
  • some of the encode calls of the encoding containers

Could you direct me on what do I do to trigger those methods? Or are they some obsolete requirements or requirements that are only called for a specific encoder like JSONEncoder?

2 Likes

tl;dr: You've hit on pretty much all code paths through these protocols that are only hit when called manually by a type implementing encode(to:).

In order of "WHEN IS IT CALLED?":

  1. encodeNil(forKey:) is only hit if a type calls it on a keyed encoding container, to explicitly store a nil value for a key when the underlying type doesn't matter. This is a separate code path from encode<T>(_:forKey:) where T is an Optional type, and is pretty rarely called
  2. encodeConditional(_:forKey:) is only ever hit when a type calls it to encode a conditional reference to a value; this is exceedingly-rarely called
  3. nestedContainer(keyedBy:forKey:), nestedUnkeyedContainer(forKey:), superEncoder(), superEncoder(forKey:) are also only hit when a type actually requests them in order to encode nested data without introducing an additional intermediate type
  4. The non-generic encode(_:forKey:) primitive overloads are called from non-generic contexts, where the type is known directly — e.g., for primitive-type properties on a type (encode(myBoolProperty, forKey: ...)). The generic encode<T>(_:forKey:) method is called for non-primitive types (e.g. non-primitive properties on a type), and for primitive types called from a generic context (e.g., Array<T> fetches an unkeyed container and calls encode<T>(_: T), even if T is a primitive type which would otherwise dispatch to one of the concrete overloads)
    • Technically, because there's overlap between encode<T>(_:forKey:) and the non-generic overloads, you could skip implementing those overloads and have a single encode<T>(_:forKey:) method which satisfies those protocol requirements; however, it does mean you'd need to dynamically switch on T for all of the primitive types, which is slower than static dispatch to those methods when possible

(The same applies to all of the unkeyed/single-value variants of the above.)

If you want to add tests to exercise all of those code paths, add types which, for each container type:

  1. Call encodeNil(...)
  2. Call encodeConditional(...)
  3. Call nestedContainer(...), nestedUnkeyedContainer(...), superEncoder(), and superEncoder(...)
  4. Call encode(...) with a concrete value for each of the primitive types, as well as encode(...) for non-primitive types for generic containers containing those primitive types

That should pretty exhaustively hit all of these requirements. Hope I haven't missed anything!

8 Likes

As a related resource, when I was doing a similar task (making my own encoder), I found the Flight School Guide to Swift Codable books (now available for free) to be wonderfully explanatory and helpful in getting there. It doesn't answer the question you're asking, but it does help step into the flow that Codable requires (note: it doesn't cover the newest CodableWithConfiguration stuff though)