Using Codable With Nested JSON Is Both Easy And Fun!

Nic Laughter
4 min readFeb 2, 2018

So Codable is new, fresh, and rad. But when I first started learning how to use it, I was surprised how little information there was available for people using nested JSON structures.

TL;DR: just scroll to the bottom for the full code.

One StackOverflow answer even said you should encompass your model in another model for the server response, then just access your desired model via the initialized parent. Which is gross, don’t do that. (EDIT: sometimes this is appropriate, like if you’re parsing out several objects that all tie together and need to be initialized at the same time. I personally still don’t like it, but you do you.)

So, I’m writing this to share with the world that getting at deeper levels of JSON data with Codable is both easy, and, as aforementioned, fun!

So first let’s just take a look at a simple model.

struct Foo: Codable {    let bar: Bool    let baz: String    let bestFriend: String    let funnyGuy: String    let favoriteWeirdo: String}

As you can see, this is nothing too complicated. But what if we need to construct it from JSON data that looks like this?

{"Response": {    "Bar": true,    "Baz": "Hello, World!",    "Friends": {        "Best": "Nic",        "FunnyGuy": "Gabe",        "FavoriteWeirdo": "Jer"        }    }}

😱 Whatever shall we do?!

Well, fortunately, Codable has some pretty nifty tricks up its sleeve, and it’s all built in for you! All you need to know is how to ask for it. First, let’s declare our CodingKeys enum inside our model so the API knows what keys we’re working with here, especially since they are different than the property names we want to be using.

enum CodingKeys: String, CodingKey {    case response = "Response"    case bar = "Bar"    case baz = "Baz"    case friends = "Friends"    case bestFriend = "Best"    case funnyGuy = "FunnyGuy"    case favoriteWeirdo = "FavoriteWeirdo"}

A quick note on the naming convention here: You can name these cases anything you want, but unless you’re going to declare the values explicitly, they need to perfectly match the keys in the JSON data. So just to clarify, if you’re providing the values, you can name them whatever you’d like, but you have the option of simply naming them exactly as they come back in the JSON tree and save yourself some work.

You’ll also notice that we have declared keys for everything in our tree. Let’s take a look at how to build our model using our data and CodingKeys!

init(from decoder: Decoder) throws {    let container = try decoder.container(keyedBy: CodingKeys.self)    let response = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .response)    bar = try response.decode(Bool.self, forKey: .bar)    baz = try response.decode(String.self, forKey: .baz)    let friends = try response.nestedContainer(keyedBy: CodingKeys.self, forKey: .friends)    bestFriend = try friends.decode(String.self, forKey: .bestFriend)    funnyGuy = try friends.decode(String.self, forKey: .funnyGuy)    favoriteWeirdo = try friends.decode(String.self, forKey: .favoriteWeirdo)}

Whew! Let’s break this down. First, the container is basically our root of the JSON tree we’re working with here. Think of it as the outer-most set of brackets. Knowing that, it makes sense why we need to get to the response (by the key of “Response”), because technically that is already nested within our root container! After that it’s a breeze to get Bar and Baz out, but here comes another container! This is where the brilliance of this API really shines through: you just tell it you need another nested container! With Friends there, we can safely and easily access our last properties.

Piece of 🍰, right?!

Now, let’s take a look at how we take our pretty model and turn it into Data that we can send back to the server!

func encode(to encoder: Encoder) throws {    var container = encoder.container(keyedBy: CodingKeys.self)    var response = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .response)    try response.encode(bar, forKey: .bar)    try response.encode(baz, forKey: .baz)    var friends = response.nestedContainer(keyedBy: CodingKeys.self, forKey: .friends)    try friends.encode(bestFriend, forKey: .bestFriend)    try friends.encode(funnyGuy, forKey: .funnyGuy)    try friends.encode(favoriteWeirdo, forKey: .favoriteWeirdo)}

As you can see, you just follow the same steps! Set up the base container, then get to your Response nested container. Throw in your top-level properties, then get the nested container for your friends. Throw those in there, and voila! You’re now a master of handling nested dictionaries in a JSON tree with Codable!

Now that you’re a pro, take it to the next level! 👇🏻

Here’s the code you can throw into a playground to see how it all works together:

let jsonString = """{"Response": {    "Bar": true,    "Baz": "Hello, World!",    "Friends": {        "Best": "Nic",        "FunnyGuy": "Gabe",        "FavoriteWeirdo": "Jer"        }    }}"""let data = jsonString.data(using: .utf8)!
struct Foo: Codable { // MARK: - Properties let bar: Bool let baz: String let bestFriend: String let funnyGuy: String let favoriteWeirdo: String // MARK: - Codable // Coding Keys enum CodingKeys: String, CodingKey { case response = "Response" case bar = "Bar" case baz = "Baz" case friends = "Friends" case bestFriend = "Best" case funnyGuy = "FunnyGuy" case favoriteWeirdo = "FavoriteWeirdo" } // Decoding init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let response = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .response) bar = try response.decode(Bool.self, forKey: .bar) baz = try response.decode(String.self, forKey: .baz) let friends = try response.nestedContainer(keyedBy: CodingKeys.self, forKey: .friends) bestFriend = try friends.decode(String.self, forKey: .bestFriend) funnyGuy = try friends.decode(String.self, forKey: .funnyGuy) favoriteWeirdo = try friends.decode(String.self, forKey: .favoriteWeirdo) } // Encoding func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) var response = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .response) try response.encode(bar, forKey: .bar) try response.encode(baz, forKey: .baz) var friends = response.nestedContainer(keyedBy: CodingKeys.self, forKey: .friends) try friends.encode(bestFriend, forKey: .bestFriend) try friends.encode(funnyGuy, forKey: .funnyGuy) try friends.encode(favoriteWeirdo, forKey: .favoriteWeirdo) }}let myFoo = try! JSONDecoder().decode(Foo.self, from: data)
// Initializes a Foo object from the JSON data at the top.
let dataToSend = try! JSONEncoder().encode(myFoo)
// Turns your Foo object into raw JSON data you can send back!

--

--