Home > Back-end >  What is superEncoder for in UnkeyedEncodingContainer and KeyedEncodingContainerProtocol?
What is superEncoder for in UnkeyedEncodingContainer and KeyedEncodingContainerProtocol?

Time:03-17

I'm sorry to ask such a basic question but I haven't been able to find the answer anywhere :

In order to make an Encoder, you must define different types of containers :

  • SingleValueEncodingContainer
  • UnkeyedEncodingContainer
  • KeyedEncodingContainerProtocol (yes the naming spec is weird)

Those last two must both contain a method called superEncoder however I have not been able to find what it's supposed to do anywhere. This question has an answer that implements it but doesn't explain it, and this talk only makes a passing mention of it.

What is it supposed to do, and what is it for ?

CodePudding user response:

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:

  1. 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:)

  2. 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
  3. 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] }
    
  • Related