1

I am currently trying to encode a generic struct (T) which has an attribute of this type with JSONEncoder:

struct A <T:Codable> : Codable {
    var id: Int
    var attribute : T
    
    init(id: Int, attribute: T){
        self.id = id
        self.attribute = attribute
    }
    
}

struct B : Codable {
    var name: String
    var age: Int
}
let encoder = JSONEncoder()
let foo = A<B>(id: 1, attribute: B(name: "name", age: 29))

try? encoder.encode(foo)

This results in a JSON like this:

{
  "id" : 1,
  "attribute" : {
    "name" : "name",
    "age" : 29
  }
}

But i would like to customize the encoding to get the nested attributes to the root level:

{
  "id" : 1,
  "name" : "name",
  "age" : 29
}

Using a custom CodingKey struct didn't work for me because T might have any number of attributes and the keys (names of attributes) are not known in advance.

1 Answer 1

4

This would need to be done manually, by implementing encode(to:) and init(from:):

struct A <T:Codable> {
    var id: Int
    var attribute : T
}
extension A: Codable {
    enum CodingKeys: CodingKey { case id }

    func encode(to encoder: Encoder) throws {
       // use attribute's own encode(to:)
       try attribute.encode(to: encoder) 

       // then encode the id
       var container = encoder.container(keyedBy: CodingKeys.self)
       try container.encode(id, forKey: .id)
    }

    init(from decoder: Decoder) throws {
        // use attribute's own init(from:)
        self.attribute = try T.init(from: decoder)
        
        // decode the id
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
    }
}

Note, that this is a very brittle solution. I WOULD NOT recommend encoding it the way you planned..

It easily breaks at run-time, with an error: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0), if T's encoding container differs from A's (which is a keyed container)

For example, the following fails at run-time:

let a = A(id: 1, attribute: "A")
let data = JSONEncoder().encode(a)

This is because when T is a String, its container is a SingleValueEncodingContainer. Same would happen if T was an array:

let a = A(id: 1, attribute: ["A"])

because an array is encoded with an UnkeyedEncodingContainer

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.