Encoding and Decoding in Swift [FREE]
source link: https://www.tuicool.com/articles/uyeEniU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
A common task for iOS apps is to save data and send it over the network. But before you can do that, you need to convert the data to a suitable format through a process called encoding or serialization .
You’ll also need to convert the saved data sent over the network to a suitable format before using it in your app. This reverse process is called decoding or deserialization .
In this tutorial, you’ll learn all you need to know about encoding and decoding in Swift by managing your very own toy store. You’ll explore the following topics along the way:
- Switching between snake case and camel case formats.
- Defining custom coding keys.
- Working with keyed, unkeyed and nested containers.
- Handling nested types, dates, subclasses and polymorphic types.
There’s quite a lot to cover, so it’s time to get started! :]
Note : This tutorial assumes you have a basic knowledge of JSON . Check out this cheat sheet if you need a quick overview.
Getting Started
Download the starter playground using the Download Materials link at the top or bottom of the tutorial.
Make sure the Project navigator is visible in Xcode by going to View ▸ Navigators ▸ Show Project Navigator . Open Nested types .
Add Codable
conformance to Toy
and Employee
:
struct Toy: Codable { ... } struct Employee: Codable { ... }
Codable
isn’t a protocol on it’s own, but an alias for two other protocols: Encodable
and Decodable
. As you might guess, those two protocols declare that types can be encoded to and decoded from a different format.
You don’t need to do anything more, because all the stored properties of both Toy
and Employee
are codable. Many basic types in the Swift Standard Library and Foundation types (for example, String
and URL
) are codable by default.
Note : You can encode codable types to various formats such as Property Lists (PLists) , XML or JSON , but for this tutorial you’ll only work with JSON.
Add a JSONEncoder
and a JSONDecoder
to handle JSON
encoding and decoding of toys and employees:
let encoder = JSONEncoder() let decoder = JSONDecoder()
That’s all you need to work with JSON. Time for your first encoding and decoding challenge!
Encoding and Decoding Nested Types
Employee
contains a Toy
property — it’s a nested type . The JSON structure of your encoded employee matches the Employee
struct:
{ "name" : "John Appleseed", "id" : 7, "favoriteToy" : { "name" : "Teddy Bear" } }
public struct Employee: Codable { var name: String var id: Int var favoriteToy: Toy }
The JSON nests name
inside favoriteToy
and all the JSON keys are the same as the Employee
and Toy
stored properties, so you can easily understand the JSON structure based on your data types hierarchy. If your property names match your JSON field names, and your properties are all Codable
, then you can convert to or from JSON very easily. You’ll try that now.
The Gifts department gives employees their favorite toys as birthday gifts. Add the following code to send your employee’s data to the Gifts department:
// 1 let data = try encoder.encode(employee) // 2 let string = String(data: data, encoding: .utf8)!
Here’s how this code works:
- Encode
employee
to JSON withencode(_:)
(I told you it was easy!). - Create a string from the encoded
data
to visualize it.
Note : Press Shift-Return to run the playground up to your current line, or click the blue play button. To see results, you can print
values to the debugger console or click the Show Result button in the results sidebar.
The encoding process generates valid data, so the Gifts department can recreate the employee:
let sameEmployee = try decoder.decode(Employee.self, from: data)
Here, you’ve used decode(_:from:)
to decode data
back to Employee
… and you’ve made your employee very happy. Press the blue play button to run the Playground and see the results.
Time for your next challenge!
Switching Between Snake Case and Camel Case Formats
The Gifts department API has switched from camel case (which looksLikeThis
) to snake case (which looks_like_this_instead
) to format keys for its JSON.
But all of the stored properties of Employee
and Toy
use camel case only! Fortunately, Foundation
has you covered.
Open Snake case vs camel case and add the following code just after the encoder and decoder are created, before they get used:
encoder.keyEncodingStrategy = .convertToSnakeCase decoder.keyDecodingStrategy = .convertFromSnakeCase
Here, you set keyEncodingStrategy
to .convertToSnakeCase
to encode employee
. You also set keyDecodingStrategy
to .convertFromSnakeCase
to decode snakeData
.
Run the playground and inspect snakeString
. The encoded employee
looks like this in this case (pun intended):
{ "name" : "John Appleseed", "id" : 7, "favorite_toy" : { "name" : "Teddy Bear" } }
The formatting in JSON is now favorite_toy
and you’ve transformed it back to favoriteToy
in the Employee
struct. You’ve saved the (employee’s birth)day again! :]
Working With Custom JSON Keys
The Gifts department has changed its API again to use different JSON keys than your Employee
and Toy
stored properties use:
{ "name" : "John Appleseed", "id" : 7, "gift" : { "name" : "Teddy Bear" } }
Now, the API replaces favoriteToy
with gift
.
This means that the field names in the JSON will no longer match up with the property names in your type. You can define custom coding keys to supply coding names for your properties. You do this by adding a special enum to your type. Open Custom coding keys and add this code inside the Employee
type:
enum CodingKeys: String, CodingKey { case name, id, favoriteToy = "gift" }
CodingKeys
is the special enum mentioned above. It conforms to CodingKey
and has String
raw values. Here’s where you map favoriteToy
to gift
.
If this enum exists, only the cases present here will be used for encoding and decoding, so even if your property doesn’t require mapping, it has to be included in the enum, as name
and id
are here.
Run the playground and look at the encoded string value — you’ll see the new field name in use. The JSON doesn’t depend on your stored properties anymore, thanks to custom coding keys.
Time for your next challenge!
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(toy.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:
KeyedEncodingContainer
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:
Toy
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(toy, 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 and id the way you’re used to.
- Create a nested container
nestedContainer(keyedBy:forKey:)
and encodetoy
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.
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:
EncodingError.invalidValue(_:_:)
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.
Note : Want to learn more about polymorphism in Swift? Check out the object oriented programming tutorial: Object Oriented Programming in Swift .
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!
Working With Arrays Within Objects
The Gifts department wants to see both the names and the labels of the employees’ birthday gifts, so its API generates JSON that looks like this:
{ "name" : "Teddy Bear", "label" : [ "teddy bear", "TEDDY BEAR", "Teddy Bear" ] }
You nest the label names inside label
. The JSON structure adds an extra level of indentation compared to the previous challenge, so you need to use nested unkeyed containers for label
in this case.
Open Nested unkeyed containers and add the following code to Toy
:
func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) var labelContainer = container.nestedUnkeyedContainer(forKey: .label) try labelContainer.encode(name.lowercased()) try labelContainer.encode(name.uppercased()) try labelContainer.encode(name) }
Here you are creating a nested unkeyed container and filling it with the three label values. Run the playground and inspect string
to check the structure is right.
You can use more nested containers if your JSON has more indentation levels. Add the decoding code to the playground page:
extension Toy: Decodable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) var labelContainer = try container.nestedUnkeyedContainer(forKey: .label) var labelName = "" while !labelContainer.isAtEnd { labelName = try labelContainer.decode(String.self) } label = labelName } } let sameToy = try decoder.decode(Toy.self, from: data)
This follows the same pattern as before, working through the array and using the final value to set the value of label
, but from a nested unkeyed container.
Congratulations on completing all the challenges! :]
Encoding and decoding in Swift like a pro!
Where to Go From Here?
Download the final playground using the Download Materials button at the top or bottom of the tutorial.
If you want to learn more about encoding and decoding in Swift, check out ourSaving Data in iOS video course. It covers JSON
, Property Lists
, XML
and much more!
I hope you enjoyed this tutorial and if you have any questions or comments, please join the forum discussion below! :]
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK