Search code examples
jsonswiftcodabledecodable

Swift decode JSON array of object based on property value type


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


Solution

  • 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)
    }