Generating custom Codable implementations
Learn about when and how the generator emits a custom Codable implementation.
Overview
As much as possible, the generator tries to rely on the compiler-synthesized implementation of Codable requirements:
The synthesized implementation is used as-is for:
primitive types, such as
String
,Int
,Double
, andBool
string and integer-backed enums
arrays of other Codable types
structs generated for
object
schemas with noadditionalProperties
customization
However, a custom Codable implementation is emitted for the following types:
structs generated for
object
schemas withadditionalProperties
customizedstructs generated for
allOf
andanyOf
schemasenums with associated values generated for
oneOf
schemas
This document goes into detail about each of these types, explains why a custom Codable implementation is needed, and how it works.
Object structs with additional properties
An object
schema can have the additionalProperties
key, which documents how any properties not documented in the properties
key should be handled.
When the
additionalProperties
key is unspecified in an object schema, no custom Codable implementation is emitted and the generator lets the Swift compiler synthesize one based on the stored properties.Custom decoder: no
Custom encoder: no
When
additionalProperties: false
, any additional properties are forbidden.Custom decoder: yes, it decodes documented properties and then throws an error if any unknown properties are detected.
Custom encoder: no, since there is no storage to put them, so the user could not have accidentally created such an value of the struct, and there is no need to perform additional validation on encoding.
When
additionalProperties: true
, an extra property calledadditionalProperties
of typeOpenAPIRuntime.OpenAPIObjectContainer
is generated on the struct, in addition to any documented stored properties.Custom decoder: yes, it decodes documented properties and then collects all unknown properties into the
additionalProperties
property, which is a key-value dictionary with untyped values.Custom encoder: yes, it encodes all documented and additional properties.
When
additionalProperties: {type: ...}
, for example{type: integer}
, all unknown properties must have an integer value. An extra property calledadditionalProperties
of type (for example)[String: Int]
is generated on the struct, in addition to any documented stored properties.Custom decoder: yes, similar to 3., with the difference that values are validated to be of the specified type (for example,
Int
).Custom encoder, yes, same as 3.
Structs for allOf and anyOf
The allOf
and anyOf
schemas include one or more subschemas that all (for allOf
) or at least one (for anyOf
) need to be decodable from the underlying value.
These schemas are generated as a struct where each property maps to one subschema.
For allOf
, all properties are required, and for anyOf
, all properties are optional.
Since the subschemas are all using the same top level container for coding, the synthesized Codable implementation cannot be used and the generator always emits custom methods for coding.
An important concept referenced below is of a “key-value pair schema”. A key-value pair schema is defined as one of:
object
allOf
where all subschemas are also key-value pair schemasanyOf
oroneOf
where at least one subschema is also a key-value pair schema
The reason we make the distinction between key-value pair schemas and other schemas is because key-value pair schemas can be safely combined (you can merge two dictionaries), but other schemas cannot be safely combined (there’s no way to combine two strings, or an integer and an array, and so on). Common Swift coders, such as JSONEncoder and JSONDecoder require that we use the right methods, as you, for example, cannot encode into a single value container more than once.
The custom Codable implementations work as follows:
allOf
:Custom decoder: yes, for key-value pair schemas uses
init(from:)
of the type directly, for others decodes from a single value container.Custom encoder: yes, only encodes the first non-key-value pair schema using a single value container (reason: encoding any additional one would overwrite the first value, and the different values should persist to the same exact bytes on the wire, so only encoding the first one is safe). If no non-key-value pair subschemas are present, encodes all the key-value pair subschemas using their
encoder(to:)
method directly. (Note that anallOf
that has both non-key-value pair and key-value pair subschemas are not valid, as it’s not possible, for example, for something to be both a string and a dictionary.)
anyOf
:Custom decoder: yes, for key-value pair schemas uses
init(from:)
of the type directly, for others decodes from a single value container. The decoding is graceful, in other words any failure is turned into a nil result. But, to ensure a validanyOf
, at the end it validates that at least one subschema decoded successfully, otherwise throws an error.Custom encoder: yes, only encodes the first non-nil non-key-value pair schema using a single value container (similar to the
allOf
encoder above), then it encodes all the key-value pair subschemas using theirencoder(to:)
method directly. Note that only if all of the non-key-value pair schemas were nil will it actually encode the key-value pairs, again because ananyOf
cannot be simultaneously a single value schema (for example, a string) and a key-value pair schema (for example, a dictionary).
Enums for oneOf
A oneOf
schema represents a payload that matches exactly one subschema (but not more).
In Swift, the generator emits an enum with associated values, which matches the JSON Schema semantics.
There are two groups of oneOf schemas, which require different handling - based on whether a discriminator is present.
A discriminator is one value in the payload that encodes the name of the schema that should be used to encode and decode the payload. While normally JSON payloads are not self-describing (in other words, the code needs to know which type-safe object to decode the value into upfront), including a discriminator allows for heterogeneous collections and decoding based on this dynamic value - making the payload self-describing.
Including a discriminator restricts the subschemas to be object-ish (objects and allOf/anyOf/oneOf of object-ish subschemas) schemas, as no other schemas can have properties, and a discriminator is always a property. However, it allows direct decoding, so instead of trying to decode the payload using each subschema, one by one, until one succeeds, the discriminator tells the decoder which type to use for decoding.
Has discriminator | Allowed subschemas | Decoding |
---|---|---|
Yes | Only object-ish | Direct |
No | All | Try one-by-one |
The rule of thumb is roughly as follows:
If needing to include non-object-ish schemas in a oneOf, don’t use a discriminator.
If only including object-ish schemas, use a discriminator
while not required, it is recommended for better performance and debugging.
it also allows having multiple schemas that would successfully validate from the payload, but without a discriminator would fail to be a valid oneOf, where exactly one schema must validate (but not more).
oneOf without a discriminator
Custom decoder: yes, tries to decode each subschema one-by-one, and stops when one validates correctly. For decoding the subschemas, it uses the same rules as allOf/anyOf, where key-value pair schemas are decoded directly using
init(from:)
, and other schemas are decoded from a single value container.Custom encoder: yes, a switch statement over the schema and only encodes the value matching the case of the enum, again, using the direct
encode(to:)
method for key-value pair schemas, and a single value container for all other schemas.
oneOf with a discriminator
Custom decoder: yes, performs decoding in two stages. First, decodes the discriminator property value to identify the Swift type to decode the full payload into. Then switches over the discriminator value and decodes the full payload using the chosen type. Since oneOf enums with a discriminator always contain object-ish schemas, which are key-value pair schemas, uses the direct
init(from:)
decoding initializer.Custom encoder: yes, same as oneOf without a discriminator, but always uses
encode(to:)
because only key-value pair schemas are ever used here.
A note on Foundation.Date
While Foundation.Date
technically conforms to Codable
, it’s not really codable on its own, as the method for coding is customizable by JSONEncoder/JSONDecoder (and other coders) using dateEncodingStrategy
.
This means that you cannot use Date’s init(from:)
and encode(to:)
methods directly, otherwise you always get the default date encoding, which uses a Double
- not what you usually want (ISO 8601 is more widely used).
Having to go through the container, which goes through the coder’s customized Date coding strategy is part of the reason behind some of the complexity above and why we need to make the distinction between “key-value pair schemas” and others.