Encoding and Decoding in Swift
In this tutorial, you’ll learn all about encoding and decoding in Swift, exploring the basics and advanced topics like custom dates and custom encoding. By Cosmin Pupăză.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Encoding and Decoding in Swift
25 mins
- Getting Started
- Encoding and Decoding Nested Types
- Switching Between Snake Case and Camel Case Formats
- Working With Custom JSON Keys
- Working With Flat JSON Hierarchies
- Working With Deep JSON Hierarchies
- Encoding and Decoding Dates
- Encoding and Decoding Subclasses
- Handling Arrays With Mixed Types
- Working With Arrays
- Working With Arrays Within Objects
- Where to Go From Here?
Working With Flat JSON Hierarchies
Now the Gifts department’s API doesn’t want any nested types in its JSON, so their code looks like this:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : "Teddy Bear"
}
This doesn’t match your model structure, so you need to write your own encoding logic and describe how to encode each Employee
and Toy
stored property.
To start, open Keyed containers. You’ll see an Employee
type which is declared as Encodable
. It’s also declared Decodable
in an extension. This split is to keep the free member-wise initializer you get with Swift struct
s. If you declare an init
method in the main definition, you lose that. Add this code inside Employee
:
// 1
enum CodingKeys: CodingKey {
case name, id, gift
}
func encode(to encoder: Encoder) throws {
// 2
var container = encoder.container(keyedBy: CodingKeys.self)
// 3
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
// 4
try container.encode(favoriteToy.name, forKey: .gift)
}
For simple cases like you’ve seen above, encode(to:)
is automatically implemented for you by the compiler. Now, you’re doing it yourself. Here’s what the code is doing:
- Create a set of coding keys to represent your JSON fields. Because you’re not doing any mapping, you don’t need to declare them as strings since there are no raw values.
- Create a
KeyedEncodingContainer
. This is like a dictionary you can store your properties in as you encode them. - Encode the
name
andid
properties directly to the container. - Encode the toy’s name directly into the container, using the
gift
key.
Run the playground and inspect the value of the encoded string – it will match the JSON at the top of this section. Being able to choose which properties to encode for which keys gives you a lot of flexibility.
The decoding process is the opposite of the encoding process. Replace the scary fatalError("To do")
with this:
// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
// 2
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
// 3
let gift = try container.decode(String.self, forKey: .gift)
favoriteToy = Toy(name: gift)
As with encoding, for simple cases init(from:)
is made automatically for you by the compiler, but here you’re doing it yourself. Here’s what the code is doing:
- Get a keyed container from the decoder, this will contain all of the properties in the JSON.
- Extract the
name
andid
values from the container using the appropriate type and coding key. - Extract the gift’s name, and use that to build a
Toy
and assign it to the correct property.
Add a line to recreate an employee from your flat JSON:
let sameEmployee = try decoder.decode(Employee.self, from: data)
This time, you have chosen which properties to decode for which keys and had the chance to do further work during decoding. Manual encoding and decoding is powerful and gives you flexibility. You’ll learn more about this in the next challenges.
Working With Deep JSON Hierarchies
The Gifts department wants to make sure that the employees’ birthday gifts can only be toys, so its API generates JSON that looks like this:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"toy" : {
"name" : "Teddy Bear"
}
}
}
You nest name
inside toy
and toy
inside gift
. The JSON structure adds an extra level of indentation compared to the Employee
hierarchy, so you need to use nested keyed containers for gift
in this case.
Open Nested keyed containers and add the following code to Employee
:
// 1
enum CodingKeys: CodingKey {
case name, id, gift
}
// 2
enum GiftKeys: CodingKey {
case toy
}
// 3
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
// 4
var giftContainer = container
.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
try giftContainer.encode(favoriteToy, forKey: .toy)
}
This is how the above code works:
- Create your top-level coding keys.
- Create another set of coding keys, which you’ll use to create another container.
- Encode the
name
andid
the way you’re used to. - Create a nested container
nestedContainer(keyedBy:forKey:)
and encodefavoriteToy
with it.
Run the playground and inspect the encoded string to see your multi-level JSON. You may use as many nested containers as your JSON has indentation levels. This comes in handy when working with complex and deep JSON hierarchies in real world APIs.
Decoding is straightforward in this case. Add the following extension:
extension Employee: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
let giftContainer = try container
.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
}
}
let sameEmployee = try decoder.decode(Employee.self, from: nestedData)
You’ve decoded nestedData
to Employee
using a nested decoding container.
Encoding and Decoding Dates
The Gifts department needs to know the employees’ birthdays to send out the presents, so their JSON looks like this:
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : "29-05-2019",
"toy" : {
"name" : "Teddy Bear"
}
}
There is no JSON standard for dates, much to the distress of every programmer who’s ever worked with them. JSONEncoder
and JSONDecoder
will by default use a double representation of the date’s timeIntervalSinceReferenceDate
, which is not very common in the wild.
You need to use a date strategy. Add this block of code to Dates, before the try encoder.encode(employee)
statement:
// 1
extension DateFormatter {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
return formatter
}()
}
// 2
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
Here’s what this code does:
- Create a date formatter matching your desired format. It’s added as a static property on
DateFormatter
as this is good practice for your code, so formatters are reusable. - Set
dateEncodingStrategy
anddateDecodingStrategy
to.formatted(.dateFormatter)
to tell the encoder and decoder to use the formatter while encoding and decoding dates
Inspect the dateString
and check the date format is correct. You’ve made sure the Gifts department will deliver the gifts on time — way to go! :]
Just a few more challenges and you’re done.