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,

  1. How to decode arrays
  2. How to decode regular dictionaries with multiple keys
  3. How to decode nested dictionaries in two ways
  4. 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"
          }
        }
      }
    }
  ]
}

Source link

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,

  1. Since links is represented as a dictionary of keys, we will create a new struct Links under Post 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
  2. Since data is an array, we will represent it as a Swift array of objects of type Data and add three properties under it - type, id and isFavorited
  3. attributes is a nested dictionary inside data so it will be represented as Attributes property under Data struct
  4. attributes key has 3 properties - title, body, and URL, they will be represented as these keys on Attributes model
  5. Key relationships represents the deeply nested object. There are two ways to represent its values in the Decodable 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,

  1. Primitive JSON types (bool, string, double, int) are directly converted into Swift primitive types
  2. If you want to represent URLs, you can declare struct property of type URL and Decodable will convert URL string into Swift URL type

Now that we have concepts in place, let's start with #1 to #5 in the above list,

  1. Creating a Post object and adding a new object Links 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