Parsing JSON data using Codable in Swift
Oct 11, 2023Most 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 enumCodingKeys
for us, which will map all the Keys of JSON directly to theProduct
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.