Custom encoder encodes only superclass keys

I aim to create custom Encoder based on Codable API and itend to give a lot of funcionality

for free by relying on default encode(to: ) implementation and not requiring to create custom implementation.

It works as expected for structures, but with class instances which use inheritance i stumbled

on behaviour which would encode only superclass keys.


Here's the test function i'm testing:

func testNestedClassWCodingKeys() {
class L1: Codable {
  init() {
    L1_A_KEY="AAAA"
    L1_B_KEY=222
    L1_D_KEY=4.4
  }
  var L1_A_KEY: String
  var L1_B_KEY: Int
  var L1_C_KEY: Int = 2
  var L1_D_KEY: Float
}


class L2:L1 {
  override init() {
    L2_A_KEY="L2222"
    L2_B_KEY=222333
    L2_C_KEY=L3(L3_A_KEY: "L3333", L3_B_KEY: 333)
    super.init()
  }

  required init(from decoder: Decoder) throws {
    fatalError("init(from:) has not been implemented")
    try super.init(from: decoder)
  }
  var L2_A_KEY: String
  var L2_B_KEY: Int
  struct L3: Codable {
    var L3_A_KEY: String
    var L3_B_KEY: Int
  }
  var L2_C_KEY: L3
}

let t = L2()
debugPrint(t)
let encoder = TDGBinaryEncoder()
XCTAssertNoThrow( try encoder.encode(t))
}

As you see we don't use any custom encode(to:) implementations.

The first thing our encoder do when in encode()(line 44) is called is to respect Encodable encode(to: ) implementaion and call it.

func encode(_ encodable: Encodable) throws {
  //respecting default and custom implementations
  debugPrint("request for encoding",encodable)
  try encodable.encode(to: self)
}

then as expected, default encode(to:) implementation tries to encode super keys for L1.

here's the debug output:

"line 38: creating keyed container"
"line 74: encoding L1_A_KEY"
"line 74: encoding L1_B_KEY"
"line 74: encoding L1_C_KEY"
"line 74: encoding L1_D_KEY"

And then it stops...

But we expected default encoding implementation to call nested keyed container on L2

Could You please comment why it's not encoding L2 keys ?


P.S. We can send whole project for investigation

Replies

This code formatting is a pain to read. Proper indentation and line breaks would make it so much easier:


func testNestedClassWCodingKeys() { 

  class L1: Codable { 

       init() { 
            L1_A_KEY="AAAA" 
            L1_B_KEY=222 
            L1_D_KEY=4.4 
       } 

       var L1_A_KEY: String 
       var L1_B_KEY: Int 
       var L1_C_KEY: Int = 2 
       var L1_D_KEY: Float 
  }

corrected.

p.s. this is default behaviour how this forum developed to understand formating pasted from xcode

That is the expected behavior under the current implementation of Swift Codables.


Swift `Codable` is based on a compiler magic which generates two methods automatically:

- `encode(to:)` for `Encodable`

- `init(from:)` for `Decodable`


So, with your class `L1` given, Swift generates `encode(to:)` and `init(from:)` automatically, which encodes and decodes all the properties in `L1`.


By the way, Swift does not generate such a method or an initializer when they already exists, enabling users to customize the behavior of Encoding & Decoding.


And, with your class `L2`, Swift does not generate `init(from:)`, as you have explicitly defined it.

Neither, Swift does not generate `encode(to:)` as it is inherited from `L1` and already exists.


Invoking `encode(to:)` for an instance of `L2`, it calls the inherited `encode(to:)` of `L1`.


---

I do not mean this would be a desired behavior as a serialization feature of a programming language, but as for now, Swift Codable works so.

(And this would not change in the future for compatibility.)


You can find some articles searching the web with "swift codable subclasses", which shows how to write `encode(to:)` or `init(from:)` manually.


You can find some discussions about this in forums.swift.org, or you may want to start a new thread there.

I guess you are right.

Cause this is the same result with JSONEncoder:

{
  "L1_B_KEY" : 222,
  "L1_D_KEY" : 4.4000000953674316,
  "L1_A_KEY" : "AAAA"
}


I don't know what's default encode(to:) implementation.

It's just hard for me to believe it is implemented so poorly not considering inheritance.


I am wondering maybe some special behavior needed, like handling codingPath to make sublass encode keys right.

Your impression seems to be very reasonable, but it is not so simple to make your subclass conform to `Codable`.


You may need to write something like this:

class L2: L1 {
    override init() {
        L2_A_KEY="L2222"
        L2_B_KEY=222333
        L2_C_KEY=L3(L3_A_KEY: "L3333", L3_B_KEY: 333)
        super.init()
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: L2.CodingKeys.self)
        self.L2_A_KEY = try container.decode(String.self, forKey: .L2_A_KEY)
        self.L2_B_KEY = try container.decode(Int.self, forKey: .L2_B_KEY)
        self.L2_C_KEY = try container.decode(L3.self, forKey: .L2_C_KEY)
        try super.init(from: decoder)
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(L2_A_KEY, forKey: .L2_A_KEY)
        try container.encode(L2_B_KEY, forKey: .L2_B_KEY)
        try container.encode(L2_C_KEY, forKey: .L2_C_KEY)
        try super.encode(to: encoder)
    }
    
    enum CodingKeys: CodingKey {
        case L2_A_KEY
        case L2_B_KEY
        case L2_C_KEY
    }
    
    var L2_A_KEY: String
    var L2_B_KEY: Int
    struct L3: Codable {
        var L3_A_KEY: String
        var L3_B_KEY: Int
    }
    var L2_C_KEY: L3
}