Parsing JSON data using Codable in Swift

swift Oct 11, 2023
Parsing JSON data using Codable in Swift

Most applications require communication with the server in order to send and retrieve information in JSON format. In order to use the JSON data in the application, we need to store it in models in a proper format. In order to store the JSON data, we encode and decode it into a format that meets the requirements. Codable is not only works for JSON rather it works for JSON, PropertyList, and XML.

We always have the option of storing JSON data in models by parsing it manually. In this way, we have to parse each object key by key. But do we have enough time to write lots of code that might not be necessary? Obviously not.

Apple has introduced a standardized approach to decode and encode JSON data using Codable since Swift 4.

This article will explain the following things:

  • How to conform to the Codable?
  • How to decode and encode the JSON data?
  • How to parse the JSON key into different property names?
  • How to parse JSON with nested objects?
  • How to parse JSON with manual decoding?
  • How to parse JSON from non-root key?

 

We can use the Encodable and Decodable protocols for encoding and decoding the data.

Decodable (or deserialization)

By conforming to the Decodable protocol, an object can be converted from JSON to a custom object format. For example,

Consider we have a json that looks like below:

{
   name: "SwiftAnytime"
   id: 23
}

To deserialize this we would just have to create a model with properties name matching the JSON fields like:

struct Community: Decodable {
	let id: Int
	let name: String
}

And using the JSONDecoder we would be able to deserialise this like below:

let student = try? JSONDecoder().decode(Type.self, from: jsonData)

Encodable (or serialization)

An object conforming to the Encodable protocol can be converted to JSON. For example,

// creating a sample Community object:
let swiftAnytime = Community(id: 23, name: "SwiftAnytime")

let jsonData = try? JSONEncoder().encode(swiftAnytime)

Codable (not a protocol)

Codable is a type alias for the Encodable and Decodable protocols. When we use Codable as a type or a generic constraint, it matches any type that conforms to both protocols.

Predefined syntax:

typealias Codable = Decodable & Encodable

Let's take a look at some examples to see how it works.


How to conform to the Codable?

Let's take sample JSON data like the following:

{
    "id": 1,
    "title": "iPhone 9",
    "description": "An apple mobile which is nothing like apple",
    "price": 549,
    "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg",
    "images": [
        "https://i.dummyjson.com/data/products/1/1.jpg",
        "https://i.dummyjson.com/data/products/1/2.jpg",
    ]
}

The product structure should be as follows:

struct Product: Codable {
    let id: Int
    let title: String
    let shortDescription: String
    let price: Int
    let thumbnail: String
    let images: [String]
}

Explanation:

We have created a structure called Product as per the above JSON data. In this structure, we define some properties along with their data types matching to corresponding JSON fields.

Note: To convert the JSON data into a Product instance, the struct must conform to the Decodable (or Codable) protocol.

How to decode the JSON data?

Now we have to store the product information in the product model from the above JSON. For this, we have to use JSONDecoder class by passing the type and valid data object like below:

do {
    let product = try JSONDecoder().decode(Product.self, from: jsonData)
    print("title: \(product.title) and price: \(product.price)")
} catch let error {
    print("error in decoding data: \(error)")
}

Output:

title: iPhone 9 and price: 549

***Why try here with JSONDecoder()?***

JSONDecoder's decode(from) function is a throwable function and it throws an error when an invalid JSON is passed this error contains the exact information as to what keys or data types were expected and what were missing.

How to encode the JSON data?

To encode the object (i.e. product), we use JSONEncoder().encode(). We can convert the returned JSON data into a JSON string with this method. For example,

do {
    let jsonData = try JSONEncoder().encode(product)
    if let jsonString = String(data: jsonData, encoding: .utf8) {
        print("JSON String: \(jsonString)")
    }
} catch let error {
    print("error in encoding data: \(error)")
}

Output:

JSON String: {"images":["https:\/\/i.dummyjson.com\/data\/products\/1\/1.jpg","https:\/\/i.dummyjson.com\/data\/products\/1\/2.jpg"],"id":1,"title":"iPhone 9","description":"An apple mobile which is nothing like apple","price":549,"thumbnail":"https:\/\/i.dummyjson.com\/data\/products\/1\/thumbnail.jpg"}

Confused about understanding the output? There is no doubt that a large JSON string will be difficult to understand. So now what?

Don't worry, we can simply set the output format to print JSON string in the proper format like below:

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
    let jsonData = try encoder.encode(product)
    if let jsonString = String(data: jsonData, encoding: .utf8) {
        print("JSON String: \(jsonString)")
    }
} catch let error {
    print("error in encoding data: \(error)")
}

Output:

JSON String: {
  "images" : [
    "https:\/\/i.dummyjson.com\/data\/products\/1\/1.jpg",
    "https:\/\/i.dummyjson.com\/data\/products\/1\/2.jpg"
  ],
  "id" : 1,
  "title" : "iPhone 9",
  "description" : "An apple mobile which is nothing like apple",
  "price" : 549,
  "thumbnail" : "https:\/\/i.dummyjson.com\/data\/products\/1\/thumbnail.jpg"
}



How can we parse the JSON key into different property names?

In some of the cases, our property names do not match the actual keys in the JSON data. For this, we can provide alternative keys using the CodingKeys enumeration.

For example, we have to parse some keys into different properties. As in the below example, we need to parse name and shortDescription from different keys. For example,

struct Product: Codable {
    
    let id: Int
    let name: String
    let shortDescription: String
    let price: Int
    let thumbnail: String
    let images: [String]
    
    enum CodingKeys: String, CodingKey {
        case id
        case name = "title"
        case shortDescription = "description"
        case price, thumbnail, images
    }
}

After making the above changes in the Product model, the rest of the process will not need to change to encode and decode the data.

By default, if we did not use the CodingKey enum explicitly, the compiler will auto generate the enum CodingKeys for us, which will map all the Keys of JSON directly to the Product struct without change (eg: ‘id’ from JSON to ‘id’ of Product struct).

If we are using CodingKeys enum for parsing, we have to specify all the property names of a class/struct. If any property name will be missing Compiler will generate a compile-time error.



How to parse JSON with nested objects?

When a JSON object is inside another JSON object, it’s called ‘nested’, and will look like the following JSON structure:

{
    "id": 1,
    "title": "iPhone 9",
    "description": "An apple mobile which is nothing like apple",
    "price": 549,
    "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg",
    "images": [
        "https://i.dummyjson.com/data/products/1/1.jpg",
        "https://i.dummyjson.com/data/products/1/2.jpg",
    ],
    "similarProducts": [
        {
            "title": "Apple Watch",
            "price": 599,
            "thumbnailImage": "watch-thumb.jpg",
            "fullImage": "watch-full.jpg",
            "discount": 10
        },
        {
            "title": "iPhone 6",
            "price": 1000,
            "thumbnailImage": "iphone-6-thumb.jpg",
            "fullImage": "iphone-6-full.jpg"
        }
    ]
}

In the above JSON, there is a nested JSON object called similarProducts which is an array of similar products.

To store the information about similar products, we have to make some changes in Product model like the following:

struct Product: Codable {
    
    let id: Int
    let name: String
    let shortDescription: String
    let price: Int
    let thumbnail: String
    let images: [String]
    let similarProducts: [SimilarProduct]
    
    enum CodingKeys: String, CodingKey {
        case id
        case name = "title"
        case shortDescription = "description"
        case price, thumbnail, images, similarProducts
    }
}

struct SimilarProduct: Codable {
    let title: String
    let price: Int
    let detail: String
    let thumbnailImage: String
    let fullImage: String
    let discount: Int
}

After making the above changes in the Product model, the rest of the process will not need to change to encode and decode the data.

When we decode the above JSON data, we will see an error. The error will be handled in the catch block like this:

catch let error {
    print("error in decoding data: \(error)")
}

// Error Message:
// error in decoding data: keyNotFound(CodingKeys(stringValue: "discount", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "similarProducts", intValue: nil), _JSONKey(stringValue: "Index 1", intValue: 1)], debugDescription: "No value associated with key CodingKeys(stringValue: \"discount\", intValue: nil) (\"discount\").", underlyingError: nil))

By reading the error message, it clearly says that the discount key is not found in the JSON data while decoding.

We can notice that in the JSON example, the discount key is missing from the last similar product. This is a very common situation in the real world. Often, when we do JSON parsing, many keys are missing or different in the coming API responses.

How to handle this case?

We are not sure whether or not the discount key will be present in JSON data. In that case, we can make the discount property optional, as shown below:

let discount: Int?

Such properties need to be made optional since they might not appear in the JSON data.



How to parse JSON with manual decoding?

If the structure of our Swift type differs from the structure of its encoded form, we can provide a custom implementation of Encodable and Decodable to define our own encoding and decoding logic.

In the below example, the images (small and large) are nested in JSON data. We need to decode them using custom decoding logic.

Here is an example of JSON:

{
    "id": 1,
    "title": "iPhone 9",
    "description": "An apple mobile which is nothing like apple",
    "price": 549,
    "images": {
        "small": "https://i.dummyjson.com/data/products/1/thumbnail.jpg",
        "large": "https://i.dummyjson.com/data/products/1/1.jpg"
    }
}

We can see the images are nested inside another object. To decode them using a custom implementation, we have to make changes to the Product struct like the below:

struct Product: Decodable {
    
    let id: Int
    let name: String
    let shortDescription: String
    let price: Int
    let smallImage: String
    let largeImage: String
    
    enum CodingKeys: String, CodingKey {
        case id
        case name = "title"
        case shortDescription = "description"
        case price, images
    }
    
    enum ProductImageKeys: String, CodingKey {
        case small
        case large
    }
    
    init(from decoder: Decoder) throws {
        
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        name = try values.decode(String.self, forKey: .name)
        shortDescription = try values.decode(String.self, forKey: .shortDescription)
        price = try values.decode(Int.self, forKey: .price)
        
        // Here, parsing the nested json object to store in properties. We used nestedContainer() method for nested parsing.
        let productImages = try values.nestedContainer(keyedBy: ProductImageKeys.self, forKey: .images)
        smallImage = try productImages.decode(String.self, forKey: .small)
        largeImage = try productImages.decode(String.self, forKey: .large)
    }
}

We required two enumerations here because JSON data has nested objects. The Product structs should conform to the Decodable protocol and implement its initializer init(from decoder: Decoder) to support custom decoding.

In the initializer, we used the nestedContainer() method to get the nested container and decode its values (small and large images).

**Why is Codable not required here?**

If we want only decoding of the JSON data, then we have to conform to the Decodable protocol only. The Codable protocol will be required if we need to support decoding and encoding both.




Further, we will see some other examples that are helpful to understand.

How to parse JSON into an array of objects?

Example JSON:

[
    {
        "id": 1,
        "title": "iPhone 9",
        "description": "An apple mobile which is nothing like apple",
        "price": 549,
        "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg"
    },
    {
        "id": 2,
        "title": "iPhone 13",
        "description": "A superfast phone launched by Apple",
        "price": 949,
        "thumbnail": "https://i.dummyjson.com/data/products/2/thumbnail.jpg"
    }
]

In the above JSON, there is an array of objects that can be decoded like this:

let products = try JSONDecoder().decode([Product].self, from: jsonData)

How to parse JSON from non-root key?

Suppose we have a JSON data that does not have any root key like the below:

{
    "total": 2,
    "products": [
        {
            "id": 1,
            "title": "iPhone 9",
            "description": "An apple mobile which is nothing like apple",
            "price": 549,
            "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg"
        },
        {
            "id": 2,
            "title": "iPhone 13",
            "description": "A superfast phone launched by Apple",
            "price": 949,
            "thumbnail": "https://i.dummyjson.com/data/products/2/thumbnail.jpg"
        }
    ]
}

Here are the structs to decode them:

struct ProductList: Codable {
    let total: Int
    let products: [Product]
}

struct Product: Codable {
    let id: Int
    let title: String
    let description: String
    let price: Int
    let thumbnail: String
}

Here is main code to decode product list:

let productList = try JSONDecoder().decode(ProductList.self, from: jsonData)

How to decode non-nested JSON data into nested models?

Example JSON:

{
    "id": 1,
    "title": "iPhone 12",
    "price": 749,
    "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg",
    "manufacturedBy": "Apple Inc.",
    "manufacturingYear": "2022",
    "firstReleasedDate": "October 2020"
}

Example Models:

struct Product: Decodable {

    let id: Int
    let title: String
    let price: Int
    let thumnail: String
    let manufactureDetail: ManufactureDetail // making separate struct
    
    enum CodingKeys: String, CodingKey {
        case id, title, price, thumbnail, manufacturedBy, manufacturingYear, firstReleasedDate
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        title = try values.decode(String.self, forKey: .title)
        price = try values.decode(Int.self, forKey: .price)
        thumnail = try values.decode(String.self, forKey: .thumbnail)
        manufactureDetail = try ManufactureDetail(from: decoder) // decoding manufacturing details
    }
}

struct ManufactureDetail: Codable {
    
    let name: String
    let year: String
    let releasedDate: String
    
    enum CodingKeys: String, CodingKey {
        case name = "manufacturedBy"
        case year = "manufacturingYear"
        case releasedDate = "firstReleasedDate"
    }
}

After making the above changes in the Product model, the rest of the process will not need to change to decode the data.

How to decode multi-nested JSON data?

Example JSON:

{
    "id": 1,
    "title": "iPhone 12",
    "price": 749,
    "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg",
    "manufacturedDetail": {
        "company": {
            "manufacturedBy": "Apple Inc."
        }
    }
}

In the above JSON, we need to parse the value from the manufacturedBy key which is nested inside a nested object.

To decode this JSON data, we have two approaches. In the first approach, we can create multiple structs according to nested JSON. Nevertheless, it is not recommended to create multiple structs for single key parsing here.

Another approach is to parse the value using the nestedContainer() method like the below:

struct Product: Decodable {
    
    let id: Int
    let title: String
    let price: Int
    let thumnail: String
    let manufacturedBy: String
    
    enum RootKeys: String, CodingKey {
        case id, title, price, thumbnail, manufacturedDetail
    }
    
    enum ManufacturedKeys: String, CodingKey {
        case company
    }
    
    enum CompanyKeys: String, CodingKey {
        case manufacturedBy
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: RootKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        title = try values.decode(String.self, forKey: .title)
        price = try values.decode(Int.self, forKey: .price)
        thumnail = try values.decode(String.self, forKey: .thumbnail)
        
        // nested containers
        let manufacturedContainer = try values.nestedContainer(keyedBy: ManufacturedKeys.self, forKey: .manufacturedDetail)
        let companyContainer = try manufacturedContainer.nestedContainer(keyedBy: CompanyKeys.self, forKey: .company)
        self.manufacturedBy = try companyContainer.decode(String.self, forKey: .manufacturedBy)
    }
}

After making the above changes in the Product model, the rest of the process will not need to change to decode the data.

We should keep in mind some pros and cons when using the Codable:

The pros are:

  • Using it reduces a lot of code because manual parsing is no longer necessary.
  • When parsing, remove multiple if-let and guard statements.

The cons are:

  • Complex JSON data may require us to write long code.
  • If we don't handle it carefully, it won't work.

Where to go next?

Congratulations, today you learned how to encode and decode JSON data using the Codable in Swift. In iOS development, this is an imperative concept that you should learn and implement in your applications.

You can consider exploring some other articles on Protocol Oriented Programming and SwiftUI.

Signup now to get notified about our
FREE iOS Workshops!