How does automatic Codable compliance work? For the sake of extending it

You can indeed read the code to get the specifics, but to summarize the process more generally:

  1. Codable synthesis (there are actually two passes — an Encodable synthesis pass, and a Decodable synthesis pass, but they largely share work) primarily uses the CodingKeys enum nested type on a type to influence how to implement init(from:) and encode(to:)
    • Specifically, if a CodingKeys type is not found, one will be synthesized
    • The process for synthesizing a CodingKeys enum involves iterating all stored properties on the owning type — each stored property is validated for Encodable/Decodable conformance, and if everything checks out, the CodingKeys enum gets a case with the same name as the property
  2. Once the type is validated to have a CodingKeys enum (either via generation, or a user-supplied one), every case is checked against what the owning type has: every stored property needs to map 1-to-1 with an enum case (for Encodable, stored properties which don't have a matching enum case are explicitly not encoded in encode(to:), but for Decodable, every stored property needs to be assigned an enum case)
  3. Once this validation has passed, encode(to:) and init(from:) are generated by enumerating all of the cases in the CodingKeys enum, and generating an encode/encodeIfPresent/decode/decodeIfPresent call based on the type

Largely, the CodingKeys enum is the "source of truth" for what to encode and decode, and the enum cases (and their names) matter. To answer your more specific questions:

This would matter primarily if you're looking to make the tuple itself Codable and rely on the existing synthesis machinery to generate a conformance for it. Since tuples are not nominal, this wouldn't work anyway; instead, inside of encode(to:) and init(from:) generation, you could do something totally custom, making up custom names on the spot to encode into a nested container, for instance. There's flexibility here, assuming the tuple is checked for consistency up-front (e.g., all of its members are also Codable themselves)

Yes, a type can access its own synthesized CodingKeys enum — by default the enum is synthesized as private, but if you implement your own init(from:)/encode(to:) [but not both], you will be able to access it and its cases by name.


One solution for this is to generate a CodingKeys type for the tuple nested somewhere/given a private name which contains auto-generated case names, but explicit String values which contain the actual key names you're looking for, e.g.

struct Foo : Codable {
    let bar: (String, Int)

    // Synthesized by Codable conformance:
    private enum CodingKeys: String, CodingKey {
        case bar
    }

    // Synthesized by additional tuple synthesis:
    private enum _bar_CodingKeys: String, CodingKey {
        case member0 = "0"
        case member1 = "1"
    }

    // Synthesized by Codable conformance with tuple additions:
    public func encode(to encoder: Encoder) throws {
        var container = try encoder.container(keyedBy: CodingKeys.self)
        var barContainer = try encoder.nestedContainer(keyedBy: _bar_CodingKeys.self, forKey: .bar)
        try barContainer.encode(bar.0, forKey: .member0)
        try barContainer.encode(bar.1, forKey: .member1)
    }
}

This would produce a result that looks like

{ "bar": { "0": "Foo", "1": 42 } }

You can imagine that if the tuple members have labels, those labels are used:

  • (name: String, Int){ "name": "Foo", "1": 42 }
  • (name: String, identifier: Int) { "name": "Foo", "identifier": 42 }

There's also something to be said for ditching the member keys and just encoding an unkeyed container instead:

struct Foo : Codable {
    let bar: (String, Int)

    // Synthesized by Codable conformance:
    private enum CodingKeys: String, CodingKey {
        case bar
    }

    // Synthesized by Codable conformance with tuple additions:
    public func encode(to encoder: Encoder) throws {
        var container = try encoder.container(keyedBy: CodingKeys.self)
        var barContainer = try encoder.nestedUnkeyedContainer(forKey: .bar)
        try barContainer.encode(bar.0)
        try barContainer.encode(bar.1)
    }
}

This produces

{ "bar": ["Foo", "42"] }
4 Likes