Using Codable Protocol in Swift 4.1
Swift 4 Codable protocol to decode and encode json data and model objects. Explained with examples.
Apple introduced a Codable protocol in Swift 4 - A new way to encode/decode data to/from model objects. This new addition allows developers to make data types encodable and decodable thus allowing conversion to and from external JSON data.
Earlier, when developers wanted to perform decoding and encoding without built-in support, they would either needed to do it manually or use a 3rd party library to perform such operations. With Swift standard library defining a standardized approach to data encoding and decoding, it has become much easier to handle conversion from local model objects to JSON and vice-versa.
To support both encoding and decoding, all you have to do it to declare conformance to Codable, which combines the Encodable and Decodable protocols. This process is known as making your type Codable
.
As evident from Swift open source code, a Codable type is a type which conforms to both Decodable
and Encodable
protocols.
/// A type that can convert itself into
/// and out of an external representation.
public typealias Codable = Decodable & Encodable
It is important to note that it is not mandatory to use Codable
every time for all the use-cases unless your requirement is to both serialize and de-serialize JSON response.
For example, if your requirement is only to send request with JSON payload to server, you can make your struct to conform to just Encodable
protocol. On the other hand, if you want to download JSON, and convert it into model objects, you might want to go with Decodable
protocol implementation
Let's start with examples similar to the real-world use-case. Starting with easier moving on to more complicated use-cases.
- Simple Dictionary
Let's say, we want to download and decode the simple dictionary representation as follows,
Employee:
name - String
age - Int
address - String
Here is our corresponding JSON data,
{"name": "abc", "age": 40, "address": "USA"}
Let's start making a model object struct conforming to Codable
protocol capable of storing all the properties from JSON data
struct Employee: Decodable {
let name: String
let age: Int
let address: String
}
And then write a code to read JSON data and directly get the model object without further complications,
let employeeJSON = """
{"name": "abc", "age": 40, "address": "USA"}
"""
// Converting raw JSON string into Data type
let jsonData = employeeJSON.data(using: .utf8)!
do {
let decodedData = try JSONDecoder().decode(Employee.self, from: jsonData)
// Prints
// Employee(name: "abc", age: 40, address: "USA")
print(decodedData)
} catch {
print(error.localizedDescription)
}
2. What if my JSON keys are different?
This is a very valid question. If your JSON keys are different than the variable names associated with model object, Codable
implementation will get confused and will fail to map these values. In order to solve this problem, you can define custom mapping with the help of CodingKeys
protocol and define your custom keys there.
For example if you JSON looks like this,
{"employee_name": "abc", "employee_age": 40, "employee_address": "USA"}
This is how you can write code for your Employee
Codable object
struct Employee: Decodable {
let name: String
let age: Int
let address: String
enum CodingKeys: String, CodingKey {
case name = "employee_name"
case age = "employee_age"
case address = "employee_address"
}
}
Here, we're using coding keys to be able to directly map from JSON keys to object model keys. In terms of decoding, nothing changes. You can still decode JSON values the way we did in previous example,
let employeeJSON = """
{"employee_name": "abc", "employee_age": 40, "employee_address": "USA"}
"""
let jsonData = employeeJSON.data(using: .utf8)!
do {
let decodedData = try JSONDecoder().decode(Employee.self, from: jsonData)
// Prints
// Employee(name: "abc", age: 40, address: "USA")
print(decodedData)
} catch {
print(error.localizedDescription)
}
3. Custom Decoding strategy and key mapping support in Swift 4.1
By default Codable
uses keyDecodingStrategy
as .useDefaultKeys
for decoding from JSON to model objects. This maps exact keys from JSON representation to the one described in your model object - As we saw in previous two examples.
But what if your JSON keys are represented in snake case and you want to corresponding mapping from snake case to camel case of your model object? Like,
employee_name
(JSON) to employeeName
(Model object)
Swift 4.1 added a support to customize the keyDecodingStrategy
value associated with JSONDecoder
or JSONEncoder
object. If you simple want to map my_key
from JSON to myKey
in your model object, simple set keyDecodingStrategy
to convertFromSnakeCase
.
Let's look at the example. Let's see our incoming JSON looks like this,
{"employee_name": "abc", "employee_age": 40, "employee_address": "USA"}
In order to perform mapping from snake case to corresponding camel case in Swift 4.0, you might have done something like this,
struct Employee: Decodable {
let employeeName: String
let employeeAge: Int
let employeeAddress: String
enum CodingKeys: String, CodingKey {
case employeeName = "employee_name"
case employeeAge = "employee_age"
case employeeAddress = "employee_address"
}
}
And it would work. But it's a lot of code. Let's try to simplify it with our custom decoding strategy,
let employeeJSON = """
{"employee_name": "abc", "employee_age": 40, "employee_address": "USA"}
"""
let jsonData = employeeJSON.data(using: .utf8)!
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decodedData = try decoder.decode(Employee.self, from: jsonData)
// Prints
// Employee(employeeName: "abc", employeeAge: 40, employeeAddress: "USA")
print(decodedData)
} catch {
print(error.localizedDescription)
}
Which further simplifies our object model representation allowing us to take out CodingKey
implementation. The simplified and compact Employee
struct now looks like this,
struct Employee: Decodable {
let employeeName: String
let employeeAge: Int
let employeeAddress: String
}
4. Flattening nested JSON
There are certain use cases where you might want to flatten the nested JSON into simple model object. Support your JSON looks like a nested structure described below,
{"info": { "style": "kolsch", "abv": "4.9" }, "name": "lawnmower"}
Instead of adding one more level of nesting, what if we want to represent this JSON with three properties at the top level?
- style
- abv
- name
Let's take a look at the full decoding and encoding structure and I will add further explanation next to it,
struct User: Codable {
var name: String
var style: String
var abv: String
enum InfoKey: String, CodingKey {
case style
case abv
}
enum CodingKeys: String, CodingKey {
case name
case info
}
// JSON to model object
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
let info = try values.nestedContainer(keyedBy: InfoKey.self, forKey: .info)
style = try info.decode(String.self, forKey: .style)
abv = try info.decode(String.self, forKey: .abv)
}
// Model object to JSON
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var infoValue = container.nestedContainer(keyedBy: InfoKey.self, forKey: .info)
try infoValue.encode(style, forKey: .style)
try infoValue.encode(abv, forKey: .abv)
}
}
First off, we define a User
struct with three base properties we want to extract. Then we define two enums within this original struct
.
First enum
represents name
and nested info
object. We go ahead and start defining one more enum InfoKey
to be able to decode values nested in info
key.
Please note that since we're manually flattening the JSON, we need to implement init(from decoder: Decoder)
method to have the full control on decoding operation. First, we decode the top level key name
, then get the inner nested object into intermediate info
object.
We could keep it as is, but what we want to do here is flatten the original structure. Thus in the next step, we flatten it into individual properties as follows,
let info = try values.nestedContainer(keyedBy: InfoKey.self, forKey: .info)
style = try info.decode(String.self, forKey: .style)
abv = try info.decode(String.self, forKey: .abv)
In the similar way, we can write our encoder to convert from model object to equivalent JSON representation,
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var infoValue = container.nestedContainer(keyedBy: InfoKey.self, forKey: .info)
try infoValue.encode(style, forKey: .style)
try infoValue.encode(abv, forKey: .abv)
}
So now, moving on to actual example, if I want to decode JSON like following this is what I need to do,
let nestedData = """
{"info": { "style": "kolsch", "abv": "4.9" }, "name": "lawnmower"}
"""
do {
let decodedUserObject = try JSONDecoder().decode(User.self, from: nestedData.data(using: .utf8)!)
// Prints
// User(name: "lawnmower", style: "kolsch", abv: "4.9")
print(decodedUserObject)
if let encodedUserObject = try? JSONEncoder().encode(decodedUserObject) {
// Prints
// {"name":"lawnmower","info":{"style":"kolsch","abv":"4.9"}}
print(String(data: encodedUserObject,encoding: .utf8)!)
}
} catch let error {
print(error.localizedDescription)
}
5. Decoding Array of Dictionaries
So far we saw how to decode dictionaries, and ways to flatten them. Now it's the time to see how to decode simple Swift arrays. Just like previous examples, let's take a look at JSON first,
[
{"first": "Tom", "last": "Smith", "user_age": 31},
{"first": "Bob", "last": "Smith", "user_age": 28}
]
Since this JSON represents an array of dictionaries with similar structure, we will first create a simple structure matching this JSON and confirming to Codable
.
struct Human: Codable {
let firstName: String
let lastName: String
let age: Int
enum CodingKeys: String, CodingKey {
case firstName = "first"
case lastName = "last"
case age = "user_age"
}
}
Please note how we used customCodingKeys
since JSON keys do not exactly match with and map to property names forHuman
struct. For example,user_age
key in JSON gets mapped toage
inCodable
structure and so on...
Decoding JSON containing array of dictionaries has similar format as decoding individual dictionaries with a small difference that we will be putting the desired object type inside square brackets (e.g. []
).
We will see how simple the code is to convert array of dictionaries into corresponding array of Codable
objects.
let jsonData = """
[
{"first": "Tom", "last": "Smith", "user_age": 31},
{"first": "Bob", "last": "Smith", "user_age": 28}
]
""".data(using: .utf8)!
do {
let decodedData = try JSONDecoder().decode([Human].self, from: jsonData)
// Prints
// [
// CodableExercise.Human(firstName: "Tom", lastName: "Smith", age: 31),
// CodableExercise.Human(firstName: "Bob", lastName: "Smith", age: 28)
// ]
print(decodedData)
} catch {
print(error.localizedDescription)
}
It is that simple. No more magic tricks, no more complicated mappings and hassle dealing with annoying 3rd party libraries.
6. Decoding Arrays and Dictionaries together
Having seen examples of decoding dictionaries and arrays, now it the time to see how we can decode hybrid JSON containing at least one array and one dictionary. Here's the JSON we are going to use,
{
"breweries": [
{"name": "ddd", "location": "Seattle"}, {"name": "ddwewe", "location": "Washington"}
],
"pageDetails": {"page": 1, "page_count": 100}
}
Here we will have to create a top level object called Drink
with two properties nested under it,
Drink
. - pageDetails
. - breweries
First, let start by creating smaller struct PageDetails
to store drink details
struct PageDetails: Codable {
let page: Int
let pageCount: Int
enum CodingKeys: String, CodingKey {
case page
case pageCount = "page_count"
}
}
Here again, we had to use customCodingKeys
, since at least one key in JSON,page_count
does not exactly match to property namepage
associated with the model object
Similarly, let's start creating another Codable
conforming struct named Brewery
holding two properties, namely name
and location
.
struct Brewery: Codable {
let name: String
let location: String
}
Here is the special case since key names in both dictionary and struct exactly match, so we don't need to implement CodingKeys
protocol.
Once we created child models, now it the time to start creating top level object - Let's call it a Beer
Since our JSON contains two keys, first represents a dictionary, while second represents an array of dictionaries, the Beer
struct conforming to Codable
will look like this,
struct Beer: Codable {
let pageDetails: PageDetails
let breweries: [Brewery]
}
Congratulations! We successfully created Codable
structure for JSON with medium complexity level. Now, if you wish to decode the JSON into corresponding model object Beer
, the following code will help you do it:
let beersJSON = """
{"pageDetails": {"page": 1, "page_count": 100}, "breweries": [{"name": "ddd", "location": "Seattle"}, {"name": "ddwewe", "location": "Washington"}]}
"""
do {
let decodedBeerObject = try JSONDecoder().decode(Beer.self, from: breweriesJSON.data(using: .utf8)!)
// Prints
// Beer(
// pageDetails: CodableExercise.PageDetails(page: 51, pageCount: 500),
// breweries: [
// CodableExercise.Breweries(name: "ddd", location: "Seattle"),
// CodableExercise.Breweries(name: "ddwewe", location: "Washington")
// ]
// )
print(decodedBeerObject)
} catch let error {
print(error.localizedDescription)
}
7. Handling nested JSON dictionary
Now we will see slightly trickier situation. I was stumped by it for some time until I realized the solution was in fact very simple. Imagine an incoming JSON with nested dictionary structure like this,
let jsonDictionary = """
{"tom": {"first": "Tom", "last": "Smith", "user_age": 31},
"bob": {"first": "Bob", "last": "Marley", "user_age": 18},
"peter": {"first": "Peter", "last": "Pan", "user_age": 8}}
"""
Now if you try to implement it with one of the rules we saw earlier, you will notice that none of them actually apply here. You can probably try to add the one with dictionary decoding, but what about multiple keys and one dictionary for each of them then?
To begin with, let's at least start creating a human struct first,
struct Human: Codable {
let firstName: String
let lastName: String
let age: Int
enum CodingKeys: String, CodingKey {
case firstName = "first"
case lastName = "last"
case age = "user_age"
}
}
So we simply assume that we want to decode this JSON into dictionaries where key is a string and value is a Human
object. Once we have this realization, decoding logic for this JSON structure becomes relatively easy to figure out,
let jsonDictionary = """
{"tom": {"first": "Tom", "last": "Smith", "user_age": 31},
"bob": {"first": "Bob", "last": "Marley", "user_age": 18},
"peter": {"first": "Peter", "last": "Pan", "user_age": 8}}
"""
try {
let decodedDictionary = try JSONDecoder().decode([String: Human].self, from: jsonDictionary.data(using: .utf8)!)
// Prints
// [
// "tom": CodableExercise.Human(firstName: "Tom", lastName: "Smith", age: 31),
// "peter": CodableExercise.Human(firstName: "Peter", lastName: "Pan", age: 8),
// "bob": CodableExercise.Human(firstName: "Bob", lastName: "Marley", age: 18)
// ]
print(decodedDictionary)
} catch {
print(error.localizedDescription)
}
Which are exactly same values as we just read from JSON data.
Looks like this post is getting too long, even longer than I originally decided to write, so I will stop here. In the interest of time and length of this post, I will spin off couple of more posts about automatic data conversion while decoding/encoding and handling default values and exceptions while creating Codable
objects.
Update
I have added a code sample on Github. You can play with existing examples or improve it by including additional mappings. I am excited to see what you are going to do with incredible Codable protocol in your next app.
Acknowledgements:
I am thankful to several people and blogs which helped me make this tutorial. My special thanks to Ben Scheirman for the awesome post on codable protocols in Swift 4. This should be enough as long as basic and intermediate knowledge of Codable is concerned. If you have any suggestion or input that can be used to improve this article, please let me know. You can send me an email or Tweet to me @jayeshkawli on Twitter
References: