Reputation: 29326
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
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
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