Deserialize items in JSON array to different types(parameterized enum) based on fields

I am trying to deserialize a json like so

{
	"timestamp": 123456789,
	"ownerAssets" : [
		{
			"ownerId" : 123,
			"location" : "USA",
			"assets" : [
				{
					"car" : true,
					"make" : "honda",
					"model" : "crv"
				},
				{
					"fruit" : true,
					"name" : "apple",
					"sweetness" : "high"
					"count": 5
				}
			]
		},
		{
			"ownerId" : 456,
			"location" : "USA",
			"assets" : [
				{
					"car" : true,
					"make" : "toyota",
					"model" : "highlander"
				},
				{
					"fruit" : true,
					"name" : "orange",
					"sweetness" : "low",
					"count": 5
				}
			]
		}
	]
}

How do I design the DTO models to parse this? I want to avoid using protocols and prefer to use parameterized enums.

I think the following is obvious:

struct StuffDTO: Codable {
    let timestamp: Int
    let ownerAssets: [OwnerAsset]
}

extension StuffDTO {
    struct OwnerAsset: Codable {
        let ownerId: Int
        let location: String
        let assets: [Asset]
    }
}

I could define an uber object which can parse everything and if-then-else this into different parameterized enums(see below) of the same enum type

struct Asset: Codable {
    // car fields
    let car: Bool?
    let make: String?
    let model: String?
    
    // fruit fields
    let fruit: Bool?
    let name: String?
    let sweetness: String?
    let count: Int?
}

But im wondering if there is a clever way to parse json directly into parameterized enums, without parsing into an uber object.

enum Asset: Codable {
    case car(CarDTO)
    case fruit(FruitDTO)

    init() {
        // what would this custom json parsing logic look like?
    }
}

struct CarDTO: Codable {
    let make: String
    let model: String
}

struct FruitDTO: Codable {
    let name: String
    let sweetness: String
    let count: Int
}

If you had "type: String" instead of "car" / "fruit" fields it would make it a bit easier. Nevertheless assuming you have this groundwork:

struct Car: Decodable {
    let make: String
    let model: String
}

struct Fruit: Decodable {
    let name: String
    let sweetness: String
    let count: Int
}

enum Asset {
    case car(Car)
    case fruit(Fruit)
}

here are the two options that I can think of ATM:

  • Option1 (the one that uses your discriminator fields):
extension Asset: Decodable {
    enum CodingKeys: CodingKey {
        case car, fruit
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if (try? container.decodeIfPresent(Bool.self, forKey: .car)) ?? false {
            self = .car(try! Car(from: decoder))
        } else if (try? container.decodeIfPresent(Bool.self, forKey: .fruit)) ?? false {
            self = .fruit(try! Fruit(from: decoder))
        } else {
            fatalError()
        }
    }
}
  • Option2 (the one that doesn't use your discriminator fields):
extension Asset: Decodable {
    init(from decoder: Decoder) throws {
        if let v = try? Car(from: decoder) {
            self = .car(v)
        } else if let v = try? Fruit(from: decoder) {
            self = .fruit(v)
        } else {
            fatalError()
        }
    }
}

with the "type" field the first option would become a switch instead (somewhat easier to maintain and less error prone).

1 Like