Reputation: 9467
How can I use Codable
to decode JSON and cross reference the objects (not structs) as they're created? In this example I'd like the Painting
class to have arrays of the Color
objects also defined in the JSON. (I also want to be able to encode them back to JSON, too.)
Bonus: In this case I'd prefer Painting.colors
to be a non-optional let
property instead of var
. I don't want it to change after creation and I don't want it to ever be nil. (I'd rather use a default value of an empty array instead of nil.)
class Art: Codable {
var colors: [Color]?
var Paintings: [Painting]?
}
class Color: Codable {
var id: String?
var hex: String?
}
class Painting: Codable {
var name: String?
var colors: [Color]?
}
let json = """
{
"colors": [
{"id": "black","hex": "000000"
},
{"id": "red", "hex": "FF0000"},
{"id": "blue", "hex": "0000FF"},
{"id": "green", "hex": "00FF00"},
{"id": "yellow", "hex": "FFFB00"},
{"id": "orange", "hex": "FF9300"},
{"id": "purple", "hex": "FF00FF"}
],
"paintings": [
{
"name": "Starry Night",
"colorIds": ["blue", "black", "purple", "yellow"]
},
{
"name": "The Scream",
"colorIds": ["orange", "black", "blue"]
},
{
"name": "Nighthawks",
"colorIds": ["green", "orange", "blue", "yellow"]
}
]
}
"""
let data = json.data(using: .utf8)
let art = try JSONDecoder().decode(Art.self, from: data!)
Some approaches I've considered:
Manually encoding/decoding the json. Seems like a lot of extra work but maybe it gives me the control I need?
Break up JSON decoding into steps. Turn deserialize the JSON into a dictionary, pull out and decode the colors first, then the paintings (which will have access to the colors in the context). This feels like fighting against Codable
which wants you to decode all at once using Data
, not a Dictionary
.
Have Painting
dynamically find the Color
s at runtime via dynamic property. But I'd much rather get all the object relationships set up and validated and then never change, before I get on with the real work. But maybe this would be the simplest?
Not using Codable
Some other bad ideas
Upvotes: 3
Views: 2229
Reputation: 93171
I voted to reopen your question because while it's not conveniently available with JSON and Codable
, it can be done. You will have to decode the JSON manually so the question become: what is the least painful way to do it?
My rule of thumb: don't fight the JSON. Import it as-is into a Swift value and then you can do all kinds of manipulation on it. To that end, let's define a RawArt
struct that closely follow the JSON:
fileprivate struct RawArt: Decodable {
struct RawPainting: Codable {
var name: String
var colorIds: [String]
}
var colors: [Color] // the Color class matches the JSON so no need to define a new struct
var paintings: [RawPainting] // the Painting class does not so we need a substitute struct
}
And now to transform the raw JSON object to your class:
class Art: Codable {
var colors: [Color]
var paintings: [Painting]
required init(from decoder: Decoder) throws {
let rawArt = try RawArt(from: decoder)
self.colors = rawArt.colors
self.paintings = rawArt.paintings.map { rawPainting in
let name = rawPainting.name
let colors = rawPainting.colorIds.flatMap { colorId in
rawArt.colors.first(where: { $0.id == colorId })
}
return Painting(name: name, colors: colors)
}
}
}
class Color: Codable {
var id: String
var hex: String
init(id: String, hex: String) {
self.id = id
self.hex = hex
}
}
// It does not transform into the JSON you want so you may as well remove Codable conformance
class Painting: Codable {
var name: String
var colors: [Color]
init(name: String, colors: [Color]) {
self.name = name
self.colors = colors
}
}
To test that it's actually referencing a Color
object:
let data = json.data(using: .utf8)
let art = try JSONDecoder().decode(Art.self, from: data!)
art.colors[0].id = "new_black"
print(art.paintings[0].colors[1].id) // the second color in Starry Night: new_black
Everything is non optional and it takes less than 20 lines of code to unarchive the object from JSON.
Upvotes: 8