Bless Apple for introducing the Decodable , Encodable, Codable protocol in Swift 4, it makes life as an iOS developer easier phew.
An object / struct that conforms to Encodable protocol can be converted to JSON, like this :
let encodedJSONData = try? JSONEncoder().encode(car)
An object / struct that conforms to Decodable protocol can be converted from JSON, like this :
let car = try? JSONDecoder().decode(Car.self, from: jsonData)
Codable protocol is a combination of Encodable + Decodable, an object / struct conforming to Codable protocol can be converted from and to JSON.
In this post we will look into how to use the Decodable protocol to parse JSON into Object / Struct , parse struct / object into JSON and various scenario of parsing.
Table of Contents:
- Parse JSON into single struct / object
- Parse JSON into array of structs / objects
- Parse JSON from non-root key
- Parse JSON key into different property name
- Parse JSON with nested struct / object
- Parse JSON with possible null value
- Cheatsheet for Parsing JSON
Parse JSON into single struct / object
Lets start with a simple Car struct like this :
struct Car: Decodable
{
let name: String
let horsepower: Int
}
Notice the struct conforms to the Decodable protocol, so that JSON can be converted into this struct instance.
Assuming we have a JSON like this:
{
"name": "Toyota Prius",
"horsepower": 1
}
We can then parse this JSON into Car struct like this:
let url = URL(string: "https://demo0989623.mockable.io/car/1")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// ensure there is no error for this HTTP response
guard error == nil else {
print ("error: \(error!)")
return
}
// ensure there is data returned from this HTTP response
guard let data = data else {
print("No data")
return
}
// Parse JSON into Car struct using JSONDecoder
guard let car = try? JSONDecoder().decode(Car.self, from: data) else {
print("Error: Couldn't decode data into car")
return
}
// 'gotten car is Car(name: "Toyota Prius", horsepower: 1)'
print("gotten car is \(car)")
}
// execute the HTTP request
task.resume()
JSONDecoder will decode the JSON data into Car struct, Car.self tells JSONDecoder that this JSON contains properties of Car struct and decode it into Car struct.
There is a try? in front of JSONDecoder as the data you pass to the decoder might be an invalid JSON, or not even a JSON at all. When these happen, JSONDecoder().decode
method will throw an error. The Try? is used to catch error if error is thrown.
Parsing JSON into a single struct object is quite straightforward.
Parse JSON into array of structs / objects
Lets use back the Car struct:
struct Car: Decodable
{
let name: String
let horsepower: Int
}
And a JSON containing an array of cars :
[
{
"name": "Toyota Prius",
"horsepower": 1
},
{
"name": "Tesla 3",
"horsepower" : 3
},
{
"name": "Ferrari",
"horsepower" : 999
}
]
We can parse this JSON into an array of car structs like this :
let url = URL(string: "https://demo0989623.mockable.io/cars")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// ensure there is no error for this HTTP response
guard error == nil else {
print ("error: \(error!)")
return
}
// ensure there is data returned from this HTTP response
guard let data = data else {
print("No data")
return
}
// Parse JSON into array of Car struct using JSONDecoder
guard let cars = try? JSONDecoder().decode([Car].self, from: data) else {
print("Error: Couldn't decode data into cars array")
return
}
for car in cars {
print("car name is \(car.name)")
print("car horsepower is \(car.horsepower)")
print("---")
}
}
// execute the HTTP request
task.resume()
Notice the JSONDecoder().decode([Car].self, from: data)
, [Car] means array of Car, [Car].self tells the JSONDecoder that the JSON Data contains type of Array of Car struct. If Car conforms to Decodable, [Car] is Decodable and can be decoded as well.
Parse JSON from non-root key
Lets use back the Car struct:
struct Car: Decodable
{
let name: String
let horsepower: Int
}
Similar to previous JSON, we have an array of cars, but now they are inside the key of "cars" instead of on the root (most outer) of the JSON :
{
"cars": [
{
"name": "Toyota Prius",
"horsepower": 1
},
{
"name": "Tesla 3",
"horsepower" : 3
},
{
"name": "Ferrari",
"horsepower" : 999
}
]
}
We can parse this JSON into a dictionary which contain an array of car structs like this :
let url = URL(string: "https://demo0989623.mockable.io/cars")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// ensure there is no error for this HTTP response
guard error == nil else {
print ("error: \(error!)")
return
}
// ensure there is data returned from this HTTP response
guard let data = data else {
print("No data")
return
}
// Parse JSON into Dictionary that contains Array of Car struct using JSONDecoder
guard let carsArrDict = try? JSONDecoder().decode([String: [Car]].self, from: data) else {
print("Error: Couldn't decode data into dictionary of array of cars")
return
}
// if you are sure the key is "cars"
let cars = carsArrDict["cars"]!
for car in cars {
print("car name is \(car.name)")
print("car horsepower is \(car.horsepower)")
print("---")
}
}
// execute the HTTP request
task.resume()
This might start to get confusing, we will try explain [String: [Car]].self.
[Car]
means an array of Car objects.
[Int: String]
means a dictionary with Int as Keys and String as Values, eg:
var numDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
[Int: Car]
means a dictionary with Int as Keys and Car as Values, eg:
let carOne = Car(name: "Tai Lopez Lamborghini", horsepower: 88)
let carTwo = Car(name: "Toyota Highlander", horsepower: 20)
var carDict:[Int: Car] = [1: carOne, 2: carTwo]
[String: [Car]]
means a dictionary with String as Keys and Array of Cars as Values, eg:
let carOne = Car(name: "Tai Lopez Lamborghini", horsepower: 88)
let carTwo = Car(name: "Toyota Highlander", horsepower: 20)
var carArrayDict:[String: [Car]] = ["cars": [carOne, carTwo]]
[String: [Car]].self
tells the JSONDecoder to decode it into a dictionary of array of cars.
Now we know that JSONDecoder().decode([String: [Car]].self, from: data)
will parse the JSON data into a dictionary of array of cars like this : ["cars": [carOne, carTwo]]
.
To access the cars array from the dictionary, we use carsArrDict["cars"]!
. "cars" is the key in the JSON.
There is an exclaimation mark behind as compiler doesn't know if the dictionary has the key "cars", if the dictionary doesn't have the key "cars", it will return nil when we call carsArrDict["cars"]
. Since we are sure that the JSON contain the key "cars", we can force unwrap it using !
.
Parse JSON key into different property name
This time we will add a new property manufacturedAt to the Car struct to indicate which year the car was manufactured :
struct Car: Decodable
{
let name: String
let horsepower: Int
let manufacturedAt: Int
}
And the JSON with this format:
{
"name": "Fujiwara Tofu Shop Toyota AE86",
"horsepower": 100,
"manufactured_at": 1985
}
Notice that in the JSON, it's manufactured_at (with an underscore) but in the Car struct, it's manufacturedAt (camelcase).
Using the first example decoding code:
let url = URL(string: "https://demo0989623.mockable.io/car/1")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// ensure there is no error for this HTTP response
guard error == nil else {
print ("error: \(error!)")
return
}
// ensure there is data returned from this HTTP response
guard let data = data else {
print("No data")
return
}
// Parse JSON into Car struct using JSONDecoder
guard let car = try? JSONDecoder().decode(Car.self, from: data) else {
print("Error: Couldn't decode data into car")
return
}
// 'gotten car is Car(name: "Toyota Prius", horsepower: 1)'
print("gotten car is \(car)")
}
// execute the HTTP request
task.resume()
If you try to run the code with the new JSON / Car Struct, you will get a decoding error as there is no manufactured_at property in the Car struct.
To solve this, we will add some code to tell the compiler to map manufactured_at from the JSON to manufacturedAt of the Car struct :
struct Car: Decodable
{
let name: String
let horsepower: Int
let manufacturedAt: Int
// map 'manufactured_at' from JSON to 'manufacturedAt' of Car
// 'name' and 'horsepower' are left alone as JSON already have them
enum CodingKeys : String, CodingKey {
case name
case horsepower
case manufacturedAt = "manufactured_at"
}
}
By default, if you didn't declare the CodingKeys enum (we didn't declare this for the past few examples), compiler will auto generate the enum CodingKeys for you, which will map all the Keys of JSON directly to the Car struct without change (eg: 'name' from JSON to 'name' of Car struct, 'horsepower' from JSON to 'horsepower' of Car struct).
Since we are not using the default direct mapping, as we have a different property name than the JSON key ('manufactured_at' from JSON to 'manufacturedAt' of Car struct). We will have to override the default CodingKeys by declaring it explicitly:
// map 'manufactured_at' from JSON to 'manufacturedAt' of Car
// 'name' and 'horsepower' are left alone as JSON already have them
enum CodingKeys : String, CodingKey {
case name
case horsepower
case manufacturedAt = "manufactured_at"
}
CodingKeys is an enum with type of String and conforms to the protocol CodingKey. Its type is string as JSON keys are usually string (eg: {"name": "GTR", "horsepower" : 250}
).
CodingKey protocol defines how properties of a struct / object are linked to its encoded form (eg: JSON).
Note:
// if we didn't specify the case value, then it
enum CodingKeys : String, CodingKey {
case name
case horsepower
case manufacturedAt = "manufactured_at"
}
// is equivalent to
enum CodingKeys : String, CodingKey {
case name = "name"
case horsepower = "horsepower"
case manufacturedAt = "manufactured_at"
}
Note 2: If we decide to override the CodingKeys enum, we have to specify all of the properties of a struct / class in CodingKeys enum. Even if we missed just one, compiler will complain :
The error message Type 'Car' does not conform to protocol 'Decodable'
doesn't inform you that the missing property in the CodingKeys needs to be filled. This might cause confusion for developer who are new to Decodable / Codable protocol 😞.
Feeling difficult to remember how to override JSON property key?
Parse JSON with nested struct / object
Lets say a manufacturer key is added into the car JSON and it contains the car manufacturer information such as name and country of origin, like this:
{
"name": "Prius",
"horsepower": 100,
"manufacturer" : {
"name": "Toyota",
"country": "Japan"
}
}
We can create another struct named "Manufacturer" and add it to the Car struct like this :
// Manufacturer.swift
struct Manufacturer: Decodable
{
let name: String
let country: String
}
// Car.swift
struct Car: Decodable
{
let name: String
let horsepower: Int
let manufacturer: Manufacturer
}
Then we just parse it using JSONDecoder as usual:
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// ensure there is no error for this HTTP response
guard error == nil else {
print ("error: \(error!)")
return
}
// ensure there is data returned from this HTTP response
guard let data = data else {
print("No data")
return
}
// Parse JSON into Car struct
guard let car = try? JSONDecoder().decode(Car.self, from: data) else {
print("Error: Couldn't decode data into car")
return
}
// car is Car(name: "Prius", horsepower: 100, manufacturer: codable.Manufacturer(name: "Toyota", country: "Japan"))
print("car is \(car)")
// car manufacturer: Manufacturer(name: "Toyota", country: "Japan")
print("car manufacturer: \(car.manufacturer)")
}
// execute the HTTP request
task.resume()
Parsing a nested struct / object is straightforward and no additional code is required on the parsing part, just add the child struct / object property to the parent.
Note:
I have removed the manufacturedAt property from Car struct to isolate away the CodingKeys from the previous section, in order to simplify the explanation of this section.
If you are wondering how to set the CodingKeys for a struct which have a child struct, you just need to specify the property of the parent struct (no need to worry about property of the child struct), like this :
// Car.swift
struct Car: Decodable
{
let name: String
let horsepower: Int
let manufacturedAt: Int
let manufacturer: Manufacturer
enum CodingKeys: String, CodingKey{
case name
case horsepower
case manufacturedAt = "manufactured_at"
case manufacturer
}
}
// Manufacturer.swift
struct Manufacturer: Decodable
{
let name: String
let country: String
}
// No need to set / override CodingKeys as keys (eg: 'name', 'country') of manufacturer in the JSON match the name of properties
Parse JSON with possible null value
Sometimes you might need to deal with JSON that have null values, lets add a key fuel_tank_capacity into the JSON to indicate how many liter of fuel the car fuel tank can hold:
[
{
"name": "Volkswagen Bettle",
"horsepower": 1,
"fuel_tank_capacity": 40
},
{
"name": "Tesla Model S",
"horsepower" : 3,
"fuel_tank_capacity": null
}
]
Since Tesla is an electric car without fuel tank, the API returns null for the fuel_tank_capacity.
Then we add fuelTankCapacity property to the Car struct like this:
struct Car: Decodable
{
let name: String
let horsepower: Int
let fuelTankCapacity: Int
enum CodingKeys: String, CodingKey{
case name
case horsepower
case fuelTankCapacity = "fuel_tank_capacity"
}
}
Lets use back the JSONDecoder code from the parsing array section :
let url = URL(string: "https://demo0989623.mockable.io/cars")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// ensure there is no error for this HTTP response
guard error == nil else {
print ("error: \(error!)")
return
}
// ensure there is data returned from this HTTP response
guard let data = data else {
print("No data")
return
}
// Parse JSON into Array of Car struct using JSONDecoder
guard let cars = try? JSONDecoder().decode([Car].self, from: data) else {
print("Error: Couldn't decode data into cars array")
return
}
for car in cars {
print("car name is \(car.name)")
print("car horsepower is \(car.horsepower)")
print("car fuel tank capacity is \(car.fuelTankCapacity)")
print("---")
}
}
// execute the HTTP request
task.resume()
If you run the code above, compiler will complain that it can't decode the JSON :
Compiler is unable to decode because in the Car struct, let fuelTankCapacity: Int
tells the compiler to expect an integer value, but the JSON returned a null
instead.
null
in JSON is equivalent to nil
in Swift. To handle null
from JSON, we have to make the fuelTankCapacity property optional like this :
struct Car: Decodable
{
let name: String
let horsepower: Int
// fuel tank capacity might be null
let fuelTankCapacity: Int?
enum CodingKeys: String, CodingKey{
case name
case horsepower
case fuelTankCapacity = "fuel_tank_capacity"
}
}
If null
is found from the JSON, it will be converted to nil
in Swift.
We then modify the print statements of cars to become like this :
for car in cars {
print("car name is \(car.name)")
print("car horsepower is \(car.horsepower)")
// only print fuelTankCapacity if it exist (ie. not null)
if let fuelTankCapacity = car.fuelTankCapacity {
print("car fuel tank capacity is \(fuelTankCapacity)")
}
print("---")
}
Then we run the code again and got this nice output:
Bonus note:
By setting a property as optional, the compiler can also handle cases where the key of property doesn't exist in the JSON, like this :
[
{
"name": "Volkswagen Bettle",
"horsepower": 1,
"fuel_tank_capacity": 40
},
{
"name": "Tesla Model S",
"horsepower" : 3
}
]
Notice that fuel_tank_capacity key is absent for the Tesla Model car, if we run the same parsing code, we will still get a nice output as compiler will set the property value as nil
if the property key doesn't exist in the JSON.
Cheatsheet
Phew this is a long post 😅.
To save you time from Googling "Parse json array swift" etc, I have made a handy cheatsheet that you can quickly refer for different cases of JSON parsing I have mentioned above.