Decoding nested keys

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.

For Stack Overflow, see this vs. this.