简体   繁体   中英

Swift JSONEncoder encoding generic type with custom structure

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM