Using Codable Protocol in iOS to Decode Complex JSON Objects
A while back, I wrote a blog post about using Codable to encode/decode JSON payload while sending to or receiving from the server. However, I often find myself in situations where having straightforward examples with a single complex JSON is easier to understand how I can use it for the current use case.
In today's post, we will take a look at how we can use Decodable to decode complex JSON objects with examples. We will go step-by-step moving from simple to more complicated scenarios. We will see,
- How to decode arrays
- How to decode regular dictionaries with multiple keys
- How to decode nested dictionaries in two ways
- How to decode URLs
To get started, let's look at the following sample JSON
{
"links": {
"first": "http://example.com/articles?page[number]=1&page[size]=1",
"prev": "http://example.com/articles?page[number]=2&page[size]=1",
"next": "http://example.com/articles?page[number]=4&page[size]=1",
"last": "http://example.com/articles?page[number]=13&page[size]=1"
},
"data": [
{
"type": "articles",
"id": "1",
"isFavorited": true,
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever.",
"url": "https://www.google.com"
},
"relationships": {
"author": {
"innerData": {
"id": "42",
"type": "people"
}
}
}
}
]
}
To represent this JSON into Swift, we will create a top-level object and organize JSON fields under its umbrella. Let's call this top-level object a Post
. In the same way, we will organize subfields as follows,
- Since
links
is represented as a dictionary of keys, we will create a new structLinks
underPost
whose properties will represent first, prev, next and last links sent by this JSON. If we end up adding a new key to this dictionary, we need to change our model - Since
data
is an array, we will represent it as a Swift array of objects of typeData
and add three properties under it -type
,id
andisFavorited
attributes
is a nested dictionary insidedata
so it will be represented asAttributes
property underData
structattributes
key has 3 properties - title, body, and URL, they will be represented as these keys onAttributes
model- Key
relationships
represents the deeply nested object. There are two ways to represent its values in theDecodable
object
a. Directly store id
and type
on the top-level object Data
b. Create a chain of nested objects originating from Data
struct. For example, referring to current JSON, Data
-> Relationships
-> Author
-> Data
-> ( id
and type
)
Things to note,
- Primitive JSON types (bool, string, double, int) are directly converted into Swift primitive types
- If you want to represent
URLs
, you can declare struct property of typeURL
andDecodable
will convert URL string into SwiftURL
type
Now that we have concepts in place, let's start with #1 to #5 in the above list,
- Creating a
Post
object and adding a new objectLinks
under it
struct Post: Decodable {
let links: Links
}
struct Links: Decodable {
let first: URL
let prev: URL
let next: URL
let last: URL
}
2. Create a new struct Data
and add it as an array of objects under Post
struct and its primitive properties
struct Post: Decodable {
let links: Links
let data: [Data]
}
struct Data: Decodable {
let type: String
let id: String
let isFavorited: Bool
}
3 and 4. Represent attributes
as Attributes
property under Data
struct and add all its primitive properties
struct Data: Decodable {
let type: String
let id: String
let isFavorited: Bool
let attributes: Attributes
}
struct Attributes: Decodable {
let title: String
let body: String
let url: URL
}
5.a. To decode data stored under relationships
key, extract the primitive values stored under it and store them as top-level properties on Data
object.
struct Data: Decodable {
let type: String
let id: String
let isFavorited: Bool
let attributes: Attributes
let relationshipId: String
let relationshipType: String
enum CodingKeys: String, CodingKey {
case type
case id
case isFavorited
case attributes
case relationships
}
enum RelationshipsKey: String, CodingKey {
case author
}
enum AuthorKey: String, CodingKey {
case innerData
}
enum InnerDataKey: String, CodingKey {
case id
case type
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decode(String.self, forKey: .type)
id = try values.decode(String.self, forKey: .id)
isFavorited = try values.decode(Bool.self, forKey: .isFavorited)
attributes = try values.decode(Attributes.self, forKey: .attributes)
let relationships = try values.nestedContainer(keyedBy: RelationshipsKey.self, forKey: .relationships)
let author = try relationships.nestedContainer(keyedBy: AuthorKey.self, forKey: .author)
let innerData = try author.nestedContainer(keyedBy: InnerDataKey.self, forKey: .innerData)
relationshipId = try innerData.decode(String.self, forKey: .id)
relationshipType = try innerData.decode(String.self, forKey: .type)
}
}
The benefit of this approach over next is, we don't end up creating redundant object models which we don't need. We will implement init(from decoder: Decoder)
and step by step decode nested objects and only fetch and store those keys which we need instead of creating intermediate structs.
5.b. [Alternate to 5.a.] Create a chain of nested objects originating from Data
struct.
struct Data: Decodable {
let type: String
let id: String
let isFavorited: Bool
let attributes: Attributes
let relationships: Relationships
}
struct Relationships: Decodable {
let author: Author
}
struct Author: Decodable {
let innerData: InnerData
}
struct InnerData: Decodable {
let id: String
let type: String
}
In this case, we will create a new struct for each nested object under data
key and store them as properties on its parent. One disadvantage of this approach is, we keep decoding JSON children which we don't actually need. If you are going to expand on them in the future or planning to add new properties or an array of objects on them, they offer more flexibility compared to the previous approach.
Full Post
Decodable model
struct Post: Decodable {
let links: Links
let data: [Data]
}
struct Data: Decodable {
let type: String
let id: String
let isFavorited: Bool
let attributes: Attributes
// Either use line #72 or lines #73 and #74
// let relationships: Relationships
let relationshipId: String
let relationshipType: String
enum CodingKeys: String, CodingKey {
case type
case id
case isFavorited
case attributes
case relationships
}
enum RelationshipsKey: String, CodingKey {
case author
}
enum AuthorKey: String, CodingKey {
case innerData
}
enum InnerDataKey: String, CodingKey {
case id
case type
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decode(String.self, forKey: .type)
id = try values.decode(String.self, forKey: .id)
isFavorited = try values.decode(Bool.self, forKey: .isFavorited)
attributes = try values.decode(Attributes.self, forKey: .attributes)
let relationships = try values.nestedContainer(keyedBy: RelationshipsKey.self, forKey: .relationships)
let author = try relationships.nestedContainer(keyedBy: AuthorKey.self, forKey: .author)
let innerData = try author.nestedContainer(keyedBy: InnerDataKey.self, forKey: .innerData)
relationshipId = try innerData.decode(String.self, forKey: .id)
relationshipType = try innerData.decode(String.self, forKey: .type)
}
}
struct Relationships: Decodable {
let author: Author
}
struct Author: Decodable {
let innerData: InnerData
}
struct InnerData: Decodable {
let id: String
let type: String
}
struct Attributes: Decodable {
let title: String
let body: String
let url: URL
}
struct Links: Decodable {
let first: URL
let prev: URL
let next: URL
let last: URL
}
How to consume the Codable model?
Next, we will see how to consume Codable models by using dummy JSON. For the purpose of this tutorial, I have created a sample JSON using https://mocki.io. You can view it by hitting this endpoint - https://mocki.io/v1/25a4e0fd-8e39-44bf-8334-8598b3b3eff4
Now, we will write a small network code in Swift which will use URLSession
to request JSON from the endpoint. Once we download it successfully, we will use our Decodable model to decode and convert it into a local object model
let url = URL(string: "https://mocki.io/v1/25a4e0fd-8e39-44bf-8334-8598b3b3eff4")!
let urlRequest = URLRequest(url: url)
let task = session.dataTask(with: urlRequest) { (data, response, error) in
let decoder = JSONDecoder()
if let post = try? decoder.decode(Post.self, from: data!) {
print(post)
}
}
task.resume()
Running the app and verifying the output
If you run the app with the above code, you will see print
statement giving out the following output,
Post(links: Links(
first: http://example.com/articles?page%5Bnumber%5D=1&page%5Bsize%5D=1,
prev: http://example.com/articles?page%5Bnumber%5D=2&page%5Bsize%5D=1,
next: http://example.com/articles?page%5Bnumber%5D=4&page%5Bsize%5D=1,
last: http://example.com/articles?page%5Bnumber%5D=13&page%5Bsize%5D=1),
data: [Data(
type: "articles",
id: "1",
isFavorited: true,
attributes: Attributes(
title: "JSON:API paints my bikeshed!",
body: "The shortest article. Ever.",
url: https://www.google.com),
relationshipId: "42",
relationshipType: "people"
)
]
)
And that's all for today's article. Hope this gave you insights into handling complex data models using Decodable. I tried to cover many common use-cases, but if you have any unusual use-case that hasn't been covered here, please feel free to contact me on Twitter or LinkedIn for further advice. As usual, comments and feedbacks are welcome. Thanks for reading