4

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 ?

ice-wind
  • 690
  • 4
  • 20
  • 1
    `superEncoder` is for supporting inheritance in `Encodable` **classes**. – vadian Mar 16 '22 at 11:36
  • @vadian can you describe how it is used ? I'm unsure what kind of information it needs. Is it fine to just return a new encoder or do I have to maintain state from the container that contains it ? (*That would be an issue since I currently use different internal storages in each type of container, and unifying them doesn't currently seem possible*) – ice-wind Mar 16 '22 at 14:03

1 Answers1

6

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] }
    
Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • So I would have to make my ```EncodingContainer``` able to store an ```Encoder```, and then ask for its encoded data when the "real" encoding step actually happens. Is that correct ? – ice-wind Mar 16 '22 at 14:16
  • 1
    @Crysambrosia It depends entirely on how the rest of your `Encoder` is written. For instance, `JSONEncoder` [returns a new `Encoder` type which _references_ the original `Encoder`](https://github.com/apple/swift-corelibs-foundation/blob/3d1c1ad39b69aa994b52671209936e81ab28b6fd/Darwin/Foundation-swiftoverlay/JSONEncoder.swift#L546) (and basically keeps track of where it was requested to write to), and [on `deinit`](https://github.com/apple/swift-corelibs-foundation/blob/3d1c1ad39b69aa994b52671209936e81ab28b6fd/Darwin/Foundation-swiftoverlay/JSONEncoder.swift#L1010) actually does the writing – Itai Ferber Mar 16 '22 at 14:26
  • One more detail : Why does ```KeyedEncodingContainerProtocol``` have a ```superEncoder``` method **without** a key ? What am I supposed to do with that one ? – ice-wind Mar 16 '22 at 14:28
  • 1
    @Crysambrosia `superEncoder()` is ["Equivalent to calling `superEncoder(forKey:)` with `Key(stringValue: "super", intValue: 0)`."](https://developer.apple.com/documentation/swift/keyedencodingcontainerprotocol/2893423-superencoder#discussion), i.e., it encodes as if the `CodingKey` enum being used has a `super` key, and uses that key. – Itai Ferber Mar 16 '22 at 14:32
  • 1
    `JSONEncoder`, for instance, has an internal [`_JSONKey` struct](https://github.com/apple/swift-corelibs-foundation/blob/3d1c1ad39b69aa994b52671209936e81ab28b6fd/Darwin/Foundation-swiftoverlay/JSONEncoder.swift#L2520) as a utility for various internal uses, and has a convenience [`super` key](https://github.com/apple/swift-corelibs-foundation/blob/3d1c1ad39b69aa994b52671209936e81ab28b6fd/Darwin/Foundation-swiftoverlay/JSONEncoder.swift#L2544) it uses for this purpose – Itai Ferber Mar 16 '22 at 14:33
  • Thank you ! Since it is equivalent I have just made it call the other version with ```CodingKey(stringValue: "super")``` as a key, so I don't write the same logic twice. – ice-wind Mar 16 '22 at 14:52
  • @Crysambrosia Note that the other method needs to be generic on the original `CodingKey` type, which might not have a meaningful value for the string `super` (e.g., if it's an enum, `Key.init(stringValue: "super")` may return `nil`); that's why `JSONEncoder.superEncoder()` duplicates the method instead of calling the key version. – Itai Ferber Mar 16 '22 at 15:00
  • What if there is no nested data? What to do with `superEncoder` etc? – koen Mar 16 '22 at 16:10
  • 1
    @koen That's up to the `Encoder` to decide — it may decide to encode an empty container, or skip encoding anything. – Itai Ferber Mar 16 '22 at 16:11
  • 1
    (The important thing is that decoding remain valid in that case; it's more common, and probably recommended, to encode an empty container if `super` doesn't end up writing any nested data.) – Itai Ferber Mar 16 '22 at 16:27