Doug Smith
Doug Smith

Reputation: 29326

How do I decode JSON in Swift when it's an array and the first item is a different type than the rest?

Say the JSON looks like this:

[
    {
        "name": "Spot",
        "breed": "dalmation"
    },
    {
        "color": "green",
        "eats": "lettuce"
    },
    {
        "color": "brown",
        "eats": "spinach"
    },
    {
        "color": "yellow",
        "eats": "cucumbers"
    }
]

Where the first item in the JSON responses you get back from the API are always a dog, and all the ones after that are always turtles. So item 0 is dog, items 1 through N-1 are turtles.

How do I parse this into something that I can read, like:

struct Result: Codable {
    let dog: Dog
    let turtles: [Turtle]
}

Is it possible?

Upvotes: 5

Views: 2090

Answers (2)

nayem
nayem

Reputation: 7605

So your Array contains two types of element. It is a good example of Type1OrType2 problem. For this type of cases, you can consider using enum with associated type. For your case, you need a Codable enum with custom implementation of init(from:) throws & func encode(to:) throws

enum DogOrTurtle: Codable {
    case dog(Dog)
    case turtle(Turtle)

    struct Dog: Codable {
        let name: String
        let breed: String
    }

    struct Turtle: Codable {
        let color: String
        let eats: String
    }
}

extension DogOrTurtle {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            // First try to decode as a Dog, if this fails then try another
            self = try .dog(container.decode(Dog.self))
        } catch {
            do {
                // Try to decode as a Turtle, if this fails too, you have a type mismatch
                self = try .turtle(container.decode(Turtle.self))
            } catch {
                // throw type mismatch error
                throw DecodingError.typeMismatch(DogOrTurtle.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type, (Dog or Turtle)") )
            }
        }
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .dog(let dog):
            try container.encode(dog)
        case .turtle(let turtle):
            try container.encode(turtle)
        }
    }
}

With this approach, you won't need to worry about the ordering of the Dog or Turtle in your array. The elements can appear at any order and any numbers.

Usage: (I've purposefully moved the dog at the third index though)

let jsonData = """
[
    {
        "color": "green",
        "eats": "lettuce"
    },
    {
        "color": "brown",
        "eats": "spinach"
    },
    {
        "name": "Spot",
        "breed": "dalmation"
    },
    {
        "color": "yellow",
        "eats": "cucumbers"
    }
]
""".data(using: .utf8)!

do {
    let array = try JSONDecoder().decode([DogOrTurtle].self, from: jsonData)
    array.forEach { (dogOrTurtle) in
        switch dogOrTurtle {
        case .dog(let dog):
            print(dog)
        case .turtle(let turtle):
            print(turtle)
        }
    }
} catch {
    print(error)
}

Upvotes: 2

rmaddy
rmaddy

Reputation: 318854

You can implement a custom decoder for your Result struct.

init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()

    // Assume the first one is a Dog
    self.dog = try container.decode(Dog.self)

    // Assume the rest are Turtle
    var turtles = [Turtle]()

    while !container.isAtEnd {
        let turtle = try container.decode(Turtle.self)
        turtles.append(turtle)
    }

    self.turtles = turtles
}

With a minor amount of work you could support the Dog dictionary being anywhere within the array of Turtle dictionaries.

Since you declared that your structs are Codable and not just Decodable, you should also implement the custom encode(to:) from Encodable but that is an exercise left to the reader.

Upvotes: 11

Related Questions