superEncoder
in encoders and superDecoder
in decoders is a way to be able to "reserve" a nested container inside of a container, without knowing what type it will be ahead of time.
One of the main purposes for this is to support inheritance in Encodable
/Decodable
classes: a class T: Encodable
may choose to encode its contents into an UnkeyedContainer
, but its subclass U: T
may choose to encode its contents into a KeyedContainer
.
In U.encode(to:)
, U
will need to call super.encode(to:)
, and pass in an Encoder
— but it cannot pass in the Encoder
that it has received, because it has already encoded its contents in a keyed way, and it is invalid for T
to request an unkeyed container from that Encoder
. (And in general, U
won't even know what kind of container T
might want.)
The escape hatch, then, is for U
to ask its container for a nested Encoder
to be able to pass that along to its superclass. The container will make space for a nested value and create a new Encoder
which allows for writing into that reserved space. T
can then use that nested Encoder
to encode however it would like.
The result ends up looking as if U
requested a nested container and encoded the values of T
into it.
To make this a bit more concrete, consider the following:
import Foundation
class T: Encodable {
let x, y: Int
init(x: Int, y: Int) { self.x = x; self.y = y }
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(x)
try container.encode(y)
}
}
class U: T {
let z: Int
init(x: Int, y: Int, z: Int) { self.z = z; super.init(x: x, y: y) }
enum CodingKeys: CodingKey { case z }
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(z, forKey: .z)
/* How to encode T.x and T.y? */
}
}
let u = U(x: 1, y: 2, z: 3)
let data = try JSONEncoder().encode(u)
print(String(data: data, encoding: .utf8))
U
has a few options for how to encode x
and y
:
It can truly override the encoding policy of T
by including x
and y
in its CodingKeys
enum and encode them directly. This ignores how T
would prefer to encode, and if decoding is required, means that you'll have to be able to create a new T
without calling its init(from:)
It can call super.encode(to: encoder)
to have the superclass encode into the same encoder that it does. In this case, this will crash, since U
has already requested a keyed container from encoder
, and calling T.encode(to:)
will immediately request an unkeyed container from the same encoder
- In general, this may work if
T
and U
both request the same container type, but it's really not recommended to rely on. Depending on how T
encodes, it may override values that U
has already encoded
Nest T
inside of the keyed container with super.encode(to: container.superEncoder())
; this will reserve a spot in the container dictionary, create a new Encoder
, and have T
write to that encoder. The result of this, in JSON, will be:
{ "z": 3,
"super": [1, 2] }