Using Codable With Nested Arrays Is Easier And More Fun Than You Think

Nic Laughter
5 min readJul 3, 2018

--

This is a follow-up to my article on using Codable with nested JSON trees. If you haven’t read that one, first catch up there since I’ll be piggy-backing on what I’ve covered in it. 👇🏻

So now that we’re all on the same page, let’s talk about the one thing everyone needs a little help with: HOW DO I GET AN ARRAY OF OBJECTS FROM JSON RESPONSE USING CODABLE OMG?!

(For you TL;DRers, the full code is at the bottom)

Yeah, this one stumped me for a while. And honestly, I think the solution isn’t amazing, but it works for now, and in some cases is actually still better than using JSONSerialization or a third-party library. So, let’s dig into it. Below is the data I’m using for this post (you’ll notice it’s a little trickier than last time):

{"Response": {    "Bar": true,    "Baz": "Hello, World!",    "Friends": [        {"FirstName": "Gabe",        "FavoriteColor": "Orange"},        {"FirstName": "Jeremiah",        "FavoriteColor": "Green"},        {"FirstName": "Peter",        "FavoriteColor": "Red"}]}}

“Yikes, that’s trouble!” you might think. But actually, it’s not so bad! See, the real magic of Codable is in how it works. If you think back to the last article, we were able to decode properties from our JSON data simply by telling the decoder what container it’s in, what type we’re expecting out of it, and what key to use to get it. But what’s so magical about it is that Array will automatically conform to Codable if its elements also already conform to it. So just follow the same steps (tell the decoder to access a key in the container and get back the type you’re expecting), and voila! It’s really that simple.

So let’s start with a model for a friend. We want an object that holds their first name and favorite color. Following the steps from the last article, it might look something like this:

struct Friend: Codable {  let firstName: String
let favoriteColor: String
enum CodingKeys: String, CodingKey {
case firstName = "FirstName"
case favoriteColor = "FavoriteColor"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.firstName = try container.decode(String.self, forKey: .firstName)
self.favoriteColor = try container.decode(String.self, forKey: .favoriteColor)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self
try container.encode(self.firstName, forKey: .firstName)
try container.encode(self.favoriteColor, forKey: .favoriteColor)
}
}

So this seems pretty straightforward, right? We are just telling the compiler how to create this object using a Decoder that we know we’re going to provide, and how to break it down using an Encoder that we also know we’re going to provide.

‼️ DON’T MISS THIS ‼️ 👇🏻

You want to make sure you understand what’s going on here. This object is only prepared to work with data that looks like this:

{
"FirstName": String,
"FavoriteColor": String
}

BUT since this model conforms to Codable, Swift 4 automatically gives us an ability to make an Array<Friend> using the same tool! 🍰🎉

So now our response model would look like this:

struct Response: Codable {
let bar: Bool
let baz: String
let friends: [Friend]
enum CodingKeys: String, CodingKey {
case response = "Response"
case bar = "Bar"
case baz = "Baz"
case friends = "Friends"
}

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

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(self.bar, forKey: .bar)
try response.encode(self.baz, forKey: .baz)
try response.encode(self.friends, forKey: .friends)
}
}

There you have it! Of course, the most practical example of this is when you get back JSON data that has a root container of "Response" with an array of objects, but this approach will work just the same.

And that’s the waaaaaaaaaaaaaaay the news goes!

FULL CODE:

let jsonString = """
{"Response": {
"Bar": true,
"Baz": "Hello, World!",
"Friends": [
{"FirstName": "Gabe",
"FavoriteColor": "Orange"},
{"FirstName": "Jeremiah",
"FavoriteColor": "Green"},
{"FirstName": "Peter",
"FavoriteColor": "Red"}]}}
"""
struct Friend: Codable {
let firstName: String
let favoriteColor: String
enum CodingKeys: String, CodingKey {
case firstName = "FirstName"
case favoriteColor = "FavoriteColor"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.firstName = try container.decode(String.self, forKey: .firstName)
self.favoriteColor = try container.decode(String.self, forKey: .favoriteColor)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.firstName, forKey: .firstName)
try container.encode(self.favoriteColor, forKey: .favoriteColor)
}
}
struct Response: Codable {
let bar: Bool
let baz: String
let friends: [Friend]
enum CodingKeys: String, CodingKey {
case response = "Response"
case bar = "Bar"
case baz = "Baz"
case friends = "Friends"
}

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

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(self.bar, forKey: .bar)
try response.encode(self.baz, forKey: .baz)
try response.encode(self.friends, forKey: .friends)
}
}
let data = jsonString.data(using: .utf8)!// Initializes a Response object from the JSON data at the top.
let myResponse = try! JSONDecoder().decode(Response.self, from: data)
// Turns your Response object into raw JSON data you can send back!
let dataToSend = try! JSONEncoder().encode(myResponse)

Note:

I mentioned at the beginning that I don’t think this is an amazing solution, and that’s only half-true. In this instance, this obviously works great, but what would be really nice would be to expand Codable so that you could simply provide a specific key or even a path and tell the compiler to give you an object or array of objects at that path. In this example, if you don’t get or have need for the "Bar" and "Baz" properties, it kinda sucks to have to still create a Response object simply to hold your [Friends]. This is something that’s available in Objective-C and other third-party libraries like SwiftyJSON, and it would be nice to see that functionality here as well. I guess we’ll see what happens! ¯\_(ツ)_/¯

--

--