zekel
zekel

Reputation: 9467

In Swift 4, how can you use Codable to decode JSON and create references between the decoded objects?

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:

Upvotes: 3

Views: 2229

Answers (1)

Code Different
Code Different

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

Related Questions