1

How to decode an array of different JSON objects, where the same property on each object tells you what type to use to decode it:

let json =
"""
[
    {
        "@type": "FirstObject",
        "number": 1
    },
    {
        "@type": "SecondObject",
        "name": "myName"
    }
]
"""

Here is some code based on this similar answer which gets most of the way there, but fails because it doesn't know what CodingKeys are for .data:

struct FirstObject: MyData {
    var dataType: String
    var number: Int
    
    enum CodingKeys: String, CodingKey {
        case dataType = "@type"
        case number
    }
}

struct SecondObject: MyData {
    var dataType: String
    var name: String
    
    enum CodingKeys: String, CodingKey {
        case dataType = "@type"
        case name
    }
}

struct SchemaObj: Decodable
{
    var dataType: String
    var data: MyData
    
    enum CodingKeys: String, CodingKey {
        case data
        case dataType = "@type"
    }
                
    enum ParseError: Error {
        case UnknownSchemaType(Any)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        dataType = try container.decode(String.self, forKey: .dataType)
        switch dataType {
        case "FirstObject":
            data = try container.decode(FirstObject.self, forKey: .data)
        case "SecondObject":
            data = try container.decode(SecondObject.self, forKey: .data)
        default:
            throw ParseError.UnknownSchemaType(dataType)
        }
    }
}

do {
    let data = Data(json.utf8)
    let result = try JSONDecoder().decode([SchemaObj].self, from: data)
    print(result)
} catch {
    print(error)
}

Printed error is keyNotFound(CodingKeys(stringValue: "data", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"data\", intValue: nil) (\"data\").", underlyingError: nil))

Thank you

aspear
  • 78
  • 9

1 Answers1

4

You don't need the data coding key. Just decode the data property from the same decoder, based on the value of the JSON field:

enum CodingKeys: String, CodingKey {
    case dataType = "@type"
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    dataType = try container.decode(String.self, forKey: .dataType)
    switch dataType {
    case "FirstObject":
        data = try FirstObject(from: decoder)
    case "SecondObject":
        data = try SecondObject(from: decoder)
    default:
        throw ParseError.UnknownSchemaType(dataType)
    }
}

If you plan to add more types to that list, then the if/else if can can become hard to manage, you can use a lookup table to address this:

static let typeMapping: [String: MyData.Type] = [ "FirstObject": FirstObject.self ,
                                                  "SecondObject": SecondObject.self]

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let dataType = try container.decode(String.self, forKey: .dataType)
    
    guard let classToDecode = Self.typeMapping[dataType] else {
        throw ParseError.UnknownSchemaType(dataType)
    }
    
    self.dataType = dataType
    self.data = try classToDecode.init(from: decoder)
}
Cristik
  • 30,989
  • 25
  • 91
  • 127