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?
Encoding and Decoding Subclasses
The Gifts department API can handle JSON based on class hierarchies:
{
"toy" : {
"name" : "Teddy Bear"
},
"employee" : {
"name" : "John Appleseed",
"id" : 7
},
"birthday" : 580794178.33482599
}
employee
matches the base class structure which has no toy
or birthday
. Open Subclasses and make BasicEmployee
conform to Codable
:
class BasicEmployee: Codable {
This will give you an error, because GiftEmployee
is not Codable
yet. Correct that by adding the following to GiftEmployee
:
// 1
enum CodingKeys: CodingKey {
case employee, birthday, toy
}
// 2
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
birthday = try container.decode(Date.self, forKey: .birthday)
toy = try container.decode(Toy.self, forKey: .toy)
// 3
let baseDecoder = try container.superDecoder(forKey: .employee)
try super.init(from: baseDecoder)
}
This code covers decoding:
- Add the relevant coding keys.
- Decode the properties specific to the subclass.
- Use
superDecoder(forKey:)
to get a decoder instance suitable to pass to theinit(from:)
method of the superclass, then initialize the superclass.
Now implement encoding in GiftEmployee
:
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(birthday, forKey: .birthday)
try container.encode(toy, forKey: .toy)
let baseEncoder = container.superEncoder(forKey: .employee)
try super.encode(to: baseEncoder)
}
It’s the same pattern, but you use superEncoder(forKey:)
to prepare the encoder for the superclass. Add the following code to the end of the playground to test out your codable subclass:
let giftEmployee = GiftEmployee(name: "John Appleseed", id: 7, birthday: Date(),
toy: toy)
let giftData = try encoder.encode(giftEmployee)
let giftString = String(data: giftData, encoding: .utf8)!
let sameGiftEmployee = try decoder.decode(GiftEmployee.self, from: giftData)
Inspect the value of giftString
to see your work in action! You can handle even more complex class hierarchies in your apps. Time for your next challenge!
Handling Arrays With Mixed Types
The Gifts department API exposes JSON that works with different types of employees:
[
{
"name" : "John Appleseed",
"id" : 7
},
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : 580797832.94787002,
"toy" : {
"name" : "Teddy Bear"
}
}
]
This JSON array is polymorphic because it contains both default and custom employees. Open Polymorphic types and you’ll see that the different types of employee are represented by an enum. First, declare that the enum is Encodable
:
enum AnyEmployee: Encodable {
Then add this code to the body of the enum:
// 1
enum CodingKeys: CodingKey {
case name, id, birthday, toy
}
// 2
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .defaultEmployee(let name, let id):
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
case .customEmployee(let name, let id, let birthday, let toy):
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encode(birthday, forKey: .birthday)
try container.encode(toy, forKey: .toy)
case .noEmployee:
let context = EncodingError.Context(codingPath: encoder.codingPath,
debugDescription: "Invalid employee!")
throw EncodingError.invalidValue(self, context)
}
}
Here’s what’s going on with this code:
- Define enough coding keys to cover all possible cases.
- Encode valid employees and throw
EncodingError.invalidValue(_:_:)
for invalid ones.
Test your encoding by adding the following to the end of the playground:
let employees = [AnyEmployee.defaultEmployee("John Appleseed", 7),
AnyEmployee.customEmployee("John Appleseed", 7, Date(), toy)]
let employeesData = try encoder.encode(employees)
let employeesString = String(data: employeesData, encoding: .utf8)!
Inspect the value of employeesString
to see your mixed array.
Decoding is a little bit more complicated, as you have to work out what is in the JSON before you can decide how to proceed. Add the following code to the playground:
extension AnyEmployee: Decodable {
init(from decoder: Decoder) throws {
// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
let containerKeys = Set(container.allKeys)
let defaultKeys = Set<CodingKeys>([.name, .id])
let customKeys = Set<CodingKeys>([.name, .id, .birthday, .toy])
// 2
switch containerKeys {
case defaultKeys:
let name = try container.decode(String.self, forKey: .name)
let id = try container.decode(Int.self, forKey: .id)
self = .defaultEmployee(name, id)
case customKeys:
let name = try container.decode(String.self, forKey: .name)
let id = try container.decode(Int.self, forKey: .id)
let birthday = try container.decode(Date.self, forKey: .birthday)
let toy = try container.decode(Toy.self, forKey: .toy)
self = .customEmployee(name, id, birthday, toy)
default:
self = .noEmployee
}
}
}
// 4
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)
This is how it all works:
- Get a keyed container as usual, then inspect the
allKeys
property to determine which keys were present in the JSON. - Check whether the
containerKeys
matches the keys needed for a default employee or a custom employee and extract the relevant properties; otherwise, make a.noEmployee
. You could choose to throw an error here if there was no suitable default. - Decode
employeesData
to[AnyEmployee]
.
You decode each employee in employeesData
based on its concrete type, just as you do for encoding.
Only two more challenges left — time for the next one!
Working With Arrays
The Gifts department adds labels to the employees’ birthday presents; their JSON looks like this:
[
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
The JSON array contains the lower case, upper case and regular label names. You don’t need any keys this time, so you use an unkeyed container.
Open Unkeyed containers and add the encoding code to Label
:
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(toy.name.lowercased())
try container.encode(toy.name.uppercased())
try container.encode(toy.name)
}
An UnkeyedEncodingContainer
works just like the containers you’ve been using so far except… you guessed it, there are no keys. Think of it as writing to JSON arrays instead of JSON dictionaries. You encode three different strings to the container.
Run the playground and inspect labelString
to see your array.
Here’s how the decoding looks. Add the following code to the end of the playground:
extension Label: Decodable {
// 1
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var name = ""
while !container.isAtEnd {
name = try container.decode(String.self)
}
toy = Toy(name: name)
}
}
// 2
let sameLabel = try decoder.decode(Label.self, from: labelData)
This is how the above code works:
- Get the decoder’s unkeyed decoding container and loop through it with
decode(_:)
to decode the final, correctly-formatted label name. - Decode
labelData
toLabel
using your unkeyed decoding container.
You loop through the whole decoding container since the correct label name comes at the end.
Time for your last challenge!