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
You don't need the data
coding key, since there's no "data" JSON key. Just decode the data
property from the same decoder, based on the value of the JSON field:
struct SchemaObj: Decodable
{
var dataType: String
var data: MyData
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)
}
}
enum CodingKeys: String, CodingKey {
case dataType = "@type"
}
}
If you plan to add more types to that list, then the if/else if
can can become hard to manage, to improve the situation 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)
}