Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Simplify JSON parsing in Swift using Codable

11 min read 3334

Swift Logo

As an iOS developer, your work may require you to retrieve data from servers and upload data back to them. This data may contain links to images, descriptions, subscription data, or information about whether the user was successfully signed in or logged in.

For this purpose, we generally use the widely accepted JSON (JavaScript Object Notation) format. JSON is a lightweight data-interchange format that is easy for devs to read and write and simple for machines to parse and generate.

In this article, we’ll investigate simplifying JSON parsing in Swift using Codable. We’ll also review several practical JSON use cases. From simple JSON to nested polymorphic JSON, soon you’ll be able to use Codable to more easily parse and generate JSON in your mobile app!

Let’s get started!

Jump ahead:

Understanding JSON

Before we dive into the use cases, it’s important to recognize that JSON can be categorized into two different structures:

  • A collection of name/value pairs, for example:
    {
      "first_name": "Rudrank",
      "last_name": "Riyam"
    }
  • An ordered list of values as an array, for example:
    {
       "qualifications": [
          {
             "name": "high_school",
             "passed": true
          },
          {
             "name": "bachelors",
             "passed": true
          },
          {
             "name": "masters",
             "passed": false
          }
       ]
    }

Swift protocols for decoding and encoding data

Swift offers several protocols to help us change the way data is represented.

Decodable protocol

Decodable is a type that can decode itself from an external representation.

protocol Decodable

Here’s a simple JSON example:

{
  "name":"rudrank"
}

This JSON can be decoded into a structure that conforms to the Decodable protocol:

struct Information: Decodable {
  let name: String
}

This protocol has one required initializer, init(from: Decoder), that has a default implementation. When we are decoding a JSON in a custom way, we can use this initializer to provide our implementation.

Encodable protocol

Encodable is a type that can encode itself to an external representation.

protocol Encodable

Using the example above, we can use the same structure and conform to the Encodable protocol to encode an object of Information into a JSON:

struct Information: Encodable {
  let name: String
}

This protocol has one required method, func encode(to: Encoder), with a default implementation. When we encode a JSON in a custom way, we can use this method to provide our implementation.

Codable protocol

To easily parse JSON and similar formats, like XML or Property Lists (PLISTs), we can take advantage of the Codable protocol introduced in Swift 4. Codable is a typealias for Decodable and Encodable protocols, meaning it provides a new name to an existing type:

typealias Codable = Decodable & Encodable

Codable is a type that can convert itself into and out of an external representation, where the representation is JSON or a similar format.

The Swift standard library contains types like String, Int, Double, Date, Data, and URL that already conform to Codable. If we create a custom struct, enum, or class, we can conform it to the Codable protocol and use the already existing types without implementing any methods.

Apple has provided us with two classes for decoding and encoding JSON objects, JSONDecoder and JSONEncoder.



JSONDecoder class

The JSONDecoder class decodes instances of a data type from JSON objects. The data type must conform to the Decodable protocol; it can be either predefined types like String, Double, or Date or custom classes, enumerations, or structures.

With this class, we will generally use the decode_:from:) method, which has the following definition:

func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

This method takes the JSON response in the form of Data type and a generic type that conforms to the Decodable protocol. After decoding the data into the given generic type, the method returns the type.

Let’s consider again the earlier example with the object of Information that conforms to the Decodable protocol and corresponding JSON:

struct Information: Decodable {
  let name: String
}

let informationData = """
{
  "name":"rudrank"
}
""".data(using: .utf8)!

All we have to do is create an instance of the JSONDecoder and call the decode_:from:) method:

let decoder = JSONDecoder()
let information = try decoder.decode(Information.self, from: informationData)
print(information.name) // Prints "rudrank"

JSONEncoder class

We use JSONEncoder to encode, rather than decode, instances of a data type as JSON objects. With this class, we primarily use the encode(_:) method that has the following definition:

func encode<T>(_ value: T) throws -> Data where T : Encodable

This method takes a generic type that conforms to the Encodable protocol, and returns Data after encoding the type into data.

Let’s take another look at the above example:

struct Information: Encodable {
  let name: String
}

let information = Information(name: "rudrank")

We have a structure Information that we want to encode into a JSON response. Encoding a custom structure or class this way is useful when you are doing a POST request and want to add the JSON as the resource of the body of the request.

We create an instance of JSONDecoder() and use the encode(_:) method to encode the information constant:

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try encoder.encode(information)

To visualize how the JSON response looks, we create a string from the encoded data and print it:

print(String(data: data, encoding: .utf8)!) 
// Prints 
{
  "name":"rudrank"
}

This article will mainly focus on decoding and simplifying the parsing of the JSON response into structures that we can easily use in an iOS app. Once you have a good understanding of these decoding examples, encoding the type into JSON should be a trivial task.

Use Cases

The examples used in the article, from different Apple Music API endpoints, represent practical, real-world use cases.

Basic example

Let’s start with a simple example that contains only one JSON object. If we search for suggestions using the Apple Music API, we get the following response:

{
  "kind":"terms",
  "searchTerm":"the weeknd",
  "displayTerm":"the weeknd"
}

Creating a struct for this object is simple. We require the properties kind, searchTerm , and displayTerm that are of the type String:

struct Suggestion: Codable {
  let kind: String
  let searchTerm: String
  let displayTerm: String
}

To decode the JSON into a struct, we’ll use JSONDecoder():

let suggestionResponse = """
{
  "kind":"terms",
  "searchTerm":"the weeknd",
  "displayTerm":"the weeknd"
}
"""

let suggestionData = Data(suggestionResponse.utf8)
let suggestion = try JSONDecoder().decode(Suggestion.self, from: suggestionData)
print(suggestion)

// MARK: - OUTPUT
Suggestion(kind: "terms", searchTerm: "the weeknd", displayTerm: "the weeknd")

Nested example

Decoding a single JSON object is easy, but what if a key contains an object of its own, like a nested structure? To address this scenario, we’ll create another struct for the nested object, and the key property will be the type of that object.

if we search the Apple Music API genres endpoint, we get the following response:

{
  "id":"20",
  "type":"genres",
  "attributes":{
    "parentId":"34",
    "name":"Alternative",
    "parentName":"Music"
  }
}

You can see that attributes contain an object of their own, an example of a nested JSON.


More great articles from LogRocket:


To decode this, we’ll create two different struct, one for the main object and one for the nested object:

struct Genre: Codable {
  let id: String
  let type: String
  let attributes: Attributes
}

struct Attributes: Codable {
  let parentId: String
  let name: String
  let parentName: String
}

To decode the nested JSON into a struct, we’ll use JSONDecoder():

let genreResponse = """
{
  "id":"20",
  "type":"genres",
  "attributes":{
    "parentId":"34",
    "name":"Alternative",
    "parentName":"Music"
  }
}
"""

let genreData = Data(genreResponse.utf8)
let genre = try JSONDecoder().decode(Genre.self, from: genreData)

print(genre)

// MARK: - OUTPUT
Genre(id: "20", type: "genres", attributes: Attributes(parentId: "34", name: "Alternative", parentName: "Music"))

Coding keys example

In the above example, we used parentId as the variable’s name. What if we wanted to use parentID instead? Or, suppose we preferred to use parent as a variable name instead of parentName?

To address these cases, we can create alternate keys while decoding. We’ll create an enum of the type String containing all the cases as the keys and conform to the CodingKey protocol:

struct Attributes: Codable {
  let parentID: String
  let name: String
  let parent: String

  enum CodingKeys: String, CodingKey {
    case parentID = "parentId"
    case name 
    case parent = "parentName"
  }
}

Then, we’ll write our custom initializer to decode the data:

extension Attributes {
  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)

    parentID = try values.decode(String.self, forKey: .parentID)
    name = try values.decode(String.self, forKey: .name)
    parent = try values.decode(String.self, forKey: .parent)
  }
}

Null value example

Considering the above example again, let’s suppose a particular genre does not have a parent genre. In this case, the JSON object will have a null value for those keys, or those keys will be missing in the object.

In our example, there would be no keys for parentName and parentId:

{
  "id":"34",
  "type":"genres",
  "attributes":{
    "name":"Music",
  }
}

If you have been working with Swift, you are familiar with the Optional type. We can use the same concept here and update the structure Attributes to accept optional values for the parentName and parentId properties:

struct Attributes: Codable {
  let parentId: String?
  let name: String
  let parentName: String?
}

However, this approach can get cumbersome for cases with many structures. Instead of unwrapping optional values for each structure, we can write a custom initializer for the decoder and take advantage of the decodeIfPresent method:

struct Attributes: Codable {
  let parentID: String
  let name: String
  let parent: String

  enum CodingKeys: String, CodingKey {
    case parentID = "parentId"
    case name 
    case parent = "parentName"
  }
}

extension Attributes {
  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)

    parentID = try values.decodeIfPresent(String.self, forKey: .parentID) ?? ""
    name = try values.decode(String.self, forKey: .name)
    parent = try values.decodeIfPresent(String.self, forKey: .parent) ?? ""
  }
}

With this technique, we can specify a placeholder value that will be provided if the response returns a missing key or a null value.

Array example

The JSON response may contain many different objects, but it could also contain an array of objects.

Let’s go back to the simple example where we have a Suggestion struct and extend it to an array of suggestions:

{
  "results":{
    "suggestions":[
      {
        "kind":"terms",
        "searchTerm":"the weeknd",
        "displayTerm":"the weeknd"
      },
      {
        "kind":"terms",
        "searchTerm":"the weeknd & swedish house mafia",
        "displayTerm":"the weeknd & swedish house mafia"
      },
      {
        "kind":"terms",
        "searchTerm":"weeknd nigth",
        "displayTerm":"weeknd nigth"
      },
      {
        "kind":"terms",
        "searchTerm":"weeknd warriorz",
        "displayTerm":"weeknd warriorz"
      },
      {
        "kind":"terms",
        "searchTerm":"yeyo weeknd",
        "displayTerm":"yeyo weeknd"
      }
    ]
  }
}

You can see that the key suggestions contains an array of objects of type Suggestion, instead of a single Suggestion. To create structures for a JSON array, we’ll create a property of the type [Suggestion]:

struct Suggestions: Codable {
  let results: Results

  struct Results: Codable {
    let suggestions: [Suggestion]
  }
}

struct Suggestion: Codable {
  let kind: String
  let searchTerm: String
  let displayTerm: String
}

We’ll decode the data in the same manner as the previous examples, except that now the struct has a property that is an array:

let suggestionsResponse = """
{
  "results":{
    "suggestions":[
      {
        "kind":"terms",
        "searchTerm":"the weeknd",
        "displayTerm":"the weeknd"
      }
      /// rest of the JSON
    ]
  }
}
"""

let suggestionsData = Data(suggestionsResponse.utf8)
let suggestions = try JSONDecoder().decode(Suggestions.self, from: suggestionsData)

print(suggestions)

// MARK: - OUTPUT
Suggestions(results: 
Suggestions.Results(suggestions: [
Suggestion(kind: "terms", searchTerm: "the weeknd", displayTerm: "the weeknd"), 
Suggestion(kind: "terms", searchTerm: "the weeknd & swedish house mafia", displayTerm: "the weeknd & swedish house mafia"), 
Suggestion(kind: "terms", searchTerm: "weeknd nigth", displayTerm: "weeknd nigth"), 
Suggestion(kind: "terms", searchTerm: "weeknd warriorz", displayTerm: "weeknd warriorz"), 
Suggestion(kind: "terms", searchTerm: "yeyo weeknd", displayTerm: "yeyo weeknd")
])))

Complex example

By now you should have an understanding of how to work with single JSON, nested JSON, and JSON arrays. You’ll probably encounter a combination of all of these in your work as an iOS developer. The nested structure may go down three to four levels deep in the hierarchy, and the deepest structure may contain an array of objects.

Let’s take a look at an example of a stripped JSON for a personal recommendation response. It contains an array of objects under the data key, and each object contains different nested objects, like attributes, artwork , and title.

The nextUpdateDate method contains a date in the form of a string, so we can simplify the use case by decoding it as a Date in Swift:

{
  "data":[
    {
      "id":"6-27s5hU6azhJY",
      "type":"personal-recommendation",
      "attributes":{
        "resourceTypes":[
          "playlists"
        ],
        "artwork":{
          "width":1200,
          "height":1200,
          "url":"https://sampleimage.com/link/to/the/image.jpg"
        },
        "nextUpdateDate":"2022-04-16T19:00:00Z",
        "kind":"music-recommendations",
        "isGroupRecommendation":false,
        "title":{
          "stringForDisplay":"Made for You"
        }
      }
    }
  ]
}

Now, we’ll create the following structures:

struct PersonalRecommendation: Codable {
  let recommendations: [Recommendation]

  enum CodingKeys: String, CodingKey {
    case recommendations = "data"
  }
}

struct Recommendation: Codable {
  let id, type: String
  let href: URL
  let attributes: Attributes
}

struct Attributes: Codable {
  let resourceTypes: [String]
  let artwork: Artwork
  let nextUpdate: Date
  let kind: String
  let isGroupRecommendation: Bool
  let title: String

  struct Artwork: Codable {
    let width, height: Int
    let url: String
  }

  struct Title: Codable {
    let stringForDisplay: String
  }

  enum CodingKeys: String, CodingKey {
    case resourceTypes, artwork
    case nextUpdate = "nextUpdateDate"
    case kind, isGroupRecommendation
    case title
  }

  enum TitleCodingKeys: String, CodingKey {
    case stringForDisplay
  }
}

Here are the Attributes that we’ll need to decode:

extension Attributes {
  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    resourceTypes = try values.decode([String].self, forKey: .resourceTypes)
    artwork = try values.decode(Artwork.self, forKey: .artwork)
    nextUpdate = try values.decode(Date.self, forKey: .nextUpdate)
    kind = try values.decode(String.self, forKey: .kind)
    isGroupRecommendation = try values.decode(Bool.self, forKey: .isGroupRecommendation)

    let titleValues = try values.nestedContainer(keyedBy: TitleCodingKeys.self, forKey: .title)
    title = try titleValues.decode(String.self, forKey: .stringForDisplay)
  }
}

You can see how we are decoding the Attributes differently from the JSON response. The response contains the title as an object like this:

"title":{
  "stringForDisplay":"Made for You"
}

Instead of creating a variable stringForDisplay, we directly use the title in the Attributes structure. To achieve that, we create an enumeration TitleCodingKeys that holds the different keys under title object:

enum TitleCodingKeys: String, CodingKey {
  case stringForDisplay
}

Then, during the decoding process, we get the nested container of the object for the key title. Based on the nested container, we decode the stringForDisplay to the variable title:

let titleValues = try values.nestedContainer(keyedBy: TitleCodingKeys.self, forKey: .title)
title = try titleValues.decode(String.self, forKey: .stringForDisplay)

So, instead of using attributes.title.stringForDisplay, we can now directly use it as attributes.title!

The final step is to decode the response. As mentioned earlier, the response contains a date. To decode it correctly, we take advantage of the dataDecodingStrategy of JSONDecoder(). We set the property to iso8601, an international standard covering the worldwide exchange and communication of date and time-related data:

let recommendationResponse = """
{
  "data":[
    {
      "id":"6-27s5hU6azhJY",
      "type":"personal-recommendation",
      "href": "/v1/me/recommendations/6-27s5hU6azhJY",
      "attributes":{
        "resourceTypes":[
          "playlists"
        ],
        "artwork":{
          "width":1200,
          "height":1200,
          "url":"https://sampleimage.com/link/to/the/image.jpg"
        },
        "nextUpdateDate":"2022-04-16T19:00:00Z",
        "kind":"music-recommendations",
        "isGroupRecommendation":false,
        "title":{
          "stringForDisplay":"Made for You"
        }
      }
    }
  ]
}
"""

let recommendationData = Data(recommendationResponse.utf8)

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

let personalRecommendation = try decoder.decode(PersonalRecommendation.self, from: recommendationData)

print(personalRecommendation)

Dynamic objects example

Another use case that you may encounter is where the response has different objects and a few of them have the same key but others have more dynamic keys. Extending on the array example, let’s say that the response now also contains the top results.

You can see that kind is a common key in both the objects but in the first case, we have searchTerm and displayTerm keys, but the second object has a nested object content:

{
  "results":{
    "suggestions":[
      {
        "kind":"terms",
        "searchTerm":"the weeknd",
        "displayTerm":"the weeknd"
      },
      {
        "kind":"topResults",
        "content":{
          "id":"1488408568",
          "type":"songs",
          "attributes":{
            "artistName":"The Weeknd",
            "url":"https://music.apple.com/in/album/blinding-lights/1488408555?i=1488408568",
            "genreNames":[
              "R&B/Soul",
              "Music"
            ],
            "durationInMillis":201570,
            "releaseDate":"2019-11-29",
            "name":"Blinding Lights",
            "hasLyrics":true,
            "albumName":"Blinding Lights - Single"
          }
        }
      }
    ]
  }
}

One approach to decoding and parsing this JSON is to have a non-optional kind property, as well as three optional properties: searchTerm, displayTerm, and content. But, when dealing with optionals, an array of this type of data will quickly get cumbersome and will not be scalable. Can we do better? Let’s find out!

We start with a similar structure for the top-level hierarchy, but we create two different struct for terms and top results, TermSuggestion and TopResultsSuggestion:

struct TermSuggestion: Codable {
  let kind: String
  let searchTerm: String
  let displayTerm: String
}

struct TopResultsSuggestion: Codable {
  let kind: String
  let content: Content
}

struct Content: Codable {
  let id, type: String
  let attributes: Attributes
}

struct Attributes: Codable {
  let name, albumName, artistName: String
  let url: URL?
  let genres: [String]
  let duration: Int
  let releaseDate: Date
  let hasLyrics: Bool
}

extension Attributes {
  enum CodingKeys: String, CodingKey {
    case name, albumName, artistName, url
    case genres = "genreNames"
    case duration = "durationInMillis"
    case releaseDate, hasLyrics
  }

  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)

    name = try values.decode(String.self, forKey: .name)
    albumName = try values.decode(String.self, forKey: .albumName)
    artistName = try values.decode(String.self, forKey: .artistName)
    url = URL(string: try values.decode(String.self, forKey: .url))
    genres = try values.decode([String].self, forKey: .genres)
    duration = try values.decode(Int.self, forKey: .duration)
    releaseDate = try values.decode(Date.self, forKey: .releaseDate)
    hasLyrics = try values.decode(Bool.self, forKey: .hasLyrics)
  }
}

Note how the Attributes structure has an optional URL. In the initializer, we decode it as a URL from the given URL string.

We can create an enumeration, SuggestionKind, that has the coding key for kind to help us distinguish between the two use cases, terms and topResults:

enum SuggestionKind: Codable {
  case terms(TermSuggestion)
  case topResults(TopResultsSuggestion)

  enum CodingKeys: String, CodingKey {
    case kind
  }

  enum SuggestionsKind: String, Codable {
    case terms
    case topResults
  }

  public init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    let kind = try values.decode(SuggestionsKind.self, forKey: .kind)

    switch kind {
      case .terms:
        let termSuggestion = try TermSuggestion(from: decoder)
        self = .terms(termSuggestion)

      case .topResults:
        let topResultsSuggestion = try TopResultsSuggestion(from: decoder)
        self = .topResults(topResultsSuggestion)
    }
  }

  func encode(to encoder: Encoder) throws {
    var values = encoder.container(keyedBy: CodingKeys.self)

    switch self {
      case .terms(let termSuggestion):
        try values.encode(termSuggestion, forKey: .kind)

      case .topResults(let topResultsSuggestion):
        try values.encode(topResultsSuggestion, forKey: .kind)
    }
  }
}

After checking the kind key, we switch over the values to create the two cases of objects and associate them according to the relevant codable structures. In this case, these two codable structures are TermSuggestion and TopResultsSuggestion. We can take advantage of the existing decoder as both the structures are already Decodable. The Decoder does the heavy lifting for us and decodes the data.

For example, if the value of kind is terms, the decoder takes the object corresponding to the terms from the JSON and decodes it to TermSuggestion. Then, you set the decoded value to self.

For decoding, we see that there is a custom date associated with releaseDate. So, we take advantage of the property dateDecodingStrategy of JSONDecoder and pass a custom data formatter, like so:

let suggestionsData = Data(suggestionsResponse.utf8)

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)

let suggestions = try decoder.decode(Suggestions.self, from: suggestionsData)

print(suggestions)

// MARK: - OUTPUT
Suggestions(results: Suggestions.Results(suggestions: [
SuggestionKind.terms(TermSuggestion(kind: "terms", searchTerm: "the weeknd", displayTerm: "the weeknd")), 
SuggestionKind.topResults(TopResultsSuggestion(kind: "topResults", content: Content(id: "1488408568", type: "songs", attributes: Attributes(name: "Blinding Lights", albumName: "Blinding Lights - Single", artistName: "The Weeknd", url: Optional(https://music.apple.com/in/album/blinding-lights/1488408555?i=1488408568), genres: ["R&B/Soul", "Music"], duration: 201570, releaseDate: 2019-11-28 18:30:00 +0000, hasLyrics: true))))
]))

Conclusion

Parsing and generating JSON has been simplified in recent years through the use of Decodable and Encodable protocols combined with the JSONDecoder and JSONEncoder classes, respectively.

In this article, we covered several use cases for simplifying JSON parsing in Swift using the Codable protocol.

After reviewing the practical examples in the article, ranging from basic use cases to those with complex JSON objects and those with dynamic objects, I hope you feel ready to easily parse the JSON in your iOS app!

Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Leave a Reply