By Will Braynen
Your backend returns this JSON response:
{ "listener": "you", "results": { "playlist": [ { "composer": "Ravel", "title": "Pavane for a dead princess" } ] } }
But you wish your backend simply returned this instead:
{ "listener": "you", "playlist": [ { "composer": "Ravel", "title": "Pavane for a dead princess" } ] }
See how there is no “results” key and the “playlist” object is not nested inside “results”?
You ask your backend team and they admit that “results” is redundant, but are too swamped to change anything. Can you un-nest the playlist yourself? Can you reach into the JSON and grab just the keys you need? Yes you can! (If you just want to see the answer with no context, skip to section 3 below.)
1. Why you shouldn’t want a nested playlist
The short answer is: picture the call site. The longer answer is: you need to decode the service’s JSON response and so write a model in Swift using Codable. Or using Decodable since that’s all you really need here (because you are decoding the data the service sent you instead of trying to encode something to upload it to the service). Either you write this model by hand or you use https://app.quicktype.io. Either way, now your clients, when using your model, have to type out this whole thing every time they want to access the playlist or anything in it:
// Nested inside "results" :( response.results.playlist response.results.playlist.first?.composer response.results.playlist.first?.title
when instead they could, with a little bit of your help, just type this:
// Un-nested :) response.playlist response.playlist.first?.composer response.playlist.first?.title
I mean, compare this with the way you would access listener
:
// Nested: response.listener // simple response.results.playlist // why oh why // Un-nested: response.listener // simple response.playlist // simple (yay!)
Plus one less struct to maintain (e.g. the Results struct whose sole purpose is to contain the playlist).
Those are the benefits. One obvious downside, however, of wishing things to be better is that you then have to go back to writing this kind of model by hand instead of being able to automatically generate it using https://app.quicktype.io (although maybe they’ll add support for this use case in the future?). The downside becomes serious when this is not an edge case and you have a lot of models to generate.
2. Syntax I wish Swift 4 supported, but does not
I wish I could just write something like this:
enum CodingKeys: String, CodingKey { case listener case playlist = "results/playlist" }
But Codable would then instead look in the JSON for a key called “results/playlist”. That is, it would do that instead of looking for a key “results” and then looking inside that object to see if there is a “playlist” key there, as I wish it would.
In fact, Codable does not, at least at present as of Swift 4, support any of the following syntax for CodingKeys:
"results/playlist" "results.playlist" "$.results.playlist" // JsonPath syntax
So it’s not like I haven’t tried. Maybe in Swift 5?
3. Using a nested container
struct Response: Decodable { let listener: String let playlist: [MusicalPiece] enum CodingKeys: String, CodingKey { case listener // Top-level object case results // Top-level object: the container the client does not need to know about case playlist // The nested object we want to pull out of the container } // The initializer will decode the JSON data init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.listener = try container.decode(String.self, forKey: .listener) let results = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .results) self.playlist = try results.decode([MusicalPiece].self, forKey: .playlist) } }
Yay! Now your call site can look like this:
response.listener response.playlist // yay!
Download sample code, so you can play with it in Xcode’s Playground. (Written in Swift 4 using Xcode 10.1)
Further reading
See Apple’s documentation, section “Encode and Decode Manually”, which also shows how to encode. There, you will see the following: “In the examples below, the Coordinate
structure is expanded to support an elevation
property that's nested inside of an additionalInfo
container”. Once you implement func encode(to encoder: Encoder) throws
, you can upgrade Response
above from Decodable
to Codable
.