You can indeed read the code to get the specifics, but to summarize the process more generally:
Codable
synthesis (there are actually two passes — anEncodable
synthesis pass, and aDecodable
synthesis pass, but they largely share work) primarily uses theCodingKeys
enum nested type on a type to influence how to implementinit(from:)
andencode(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 forEncodable
/Decodable
conformance, and if everything checks out, theCodingKeys
enum gets a case with the same name as the property
- Specifically, if a
- 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 (forEncodable
, stored properties which don't have a matching enum case are explicitly not encoded inencode(to:)
, but forDecodable
, every stored property needs to be assigned an enum case) - Once this validation has passed,
encode(to:)
andinit(from:)
are generated by enumerating all of the cases in theCodingKeys
enum, and generating anencode
/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"] }