Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

First Edition · iOS 16 · Swift 5.8 · Xcode 14.3

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

6. Encoding & Decoding Types
Written by Eli Ganim

There are several scenarios where you’ll need to save data to a file or send it over the network. This chapter will teach you how to convert types like an Employee to a stream of bytes ready to be transported. This process is called encoding, also known as serialization.

The reverse process of turning the data into an instance is called decoding or deserialization.

Imagine you have an instance you want to write to a file. The instance itself cannot be written as-is to the file, so you need to encode it into another representation, such as a stream of bytes:

Employee ID: 7 Name: John Appleseed Employee Encoder <... a04f38bb1 ...>

Once the data is encoded and saved to a file, you can turn it back into an instance whenever you want by using a decoder:

Employee ID: 7 Name: John Appleseed Employee Decoder <... a04f38bb1 ...>

Encodable and Decodable Protocols

The Encodable protocol expresses that a type can convert itself into another representation. It declares a single method:

func encode(to: Encoder) throws

The compiler automatically generates this for you if all the stored properties of that type conform to Encodable. You’ll learn more about this later on in the chapter.

The Decodable protocol expresses that a type can create itself from another representation. It declares just a single initializer:

init(from decoder: Decoder) throws

Again, the compiler will make this initializer for you if all stored properties conform to Decodable. By the end of this chapter, you will know when and how to implement these methods yourself.

What is Codable?

Codable is a protocol to which a type can conform, which means it can be encoded and decoded. It’s an alias for the Encodable and Decodable protocols. Literally:

  typealias Codable = Encodable & Decodable

Automatic Encoding and Decoding

Many of Swift’s types are codable out of the box: Int, String, Date, Array and many other types from the Standard Library and the Foundation framework. If you want your type to be codable, the simplest way is by conforming to Codable and ensuring all its stored properties are also codable.

For example, let’s say you own a toy factory, and you have this struct to store employee data:

struct Employee {
  var name: String
  var id: Int
}

All you need to do to be able to encode and decode this type to conform to the Codable protocol, like so:

struct Employee: Codable {
  var name: String
  var id: Int
}

Wow, that was easy. You could do it because both name (String) and id (Int) are codable.

This automatic process works when you only use already Codable types. But what if your type includes other custom types as properties? For example, looking at your Employee struct, assume that it also has an optional favoriteToy property:

struct Employee: Codable {
  var name: String
  var id: Int
  var favoriteToy: Toy?
}

struct Toy: Codable {
  var name: String
}

By making sure Toy also conforms to Codable, you maintain the overall conformance to Codable for Employee as well.

All collection types, like Array and Dictionary, are also codable if they contain codable types.

Encoding and Decoding Custom Types

You can encode to or decode from several representations, such as XML or a Property List. This section will show you how to encode to and decode from JSON using Swift’s JSONEncoder and JSONDecoder classes.

JSON stands for JavaScript Object Notation and is one of the most popular ways to serialize data. It’s easily readable by humans and easy for computers to parse and generate.

For example, if you were to encode an instance of type Employee to JSON, it might look something like this:

{ "name": "John Appleseed", "id": 7 }

The conversion between an Employee type and serialized JSON is almost trivial.

JSONEncoder and JSONDecoder

Once you have a codable type, you can use JSONEncoder to convert your type to Data that can be either written to a file or sent over the network. Assume you have this employee instance:

let toy1 = Toy(name: "Teddy Bear");
let employee1 = Employee(name: "John Appleseed", id: 7, favoriteToy: toy1)

John’s birthday is coming up, and you want to give him his favorite toy as a gift. You need to send this data to the gift department. Before you can do that, you need to encode it like so:

let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(employee1)

You’ll notice that you must use try because encode(_:) might fail and throw an error.

If you try to print jsonData like this:

print(jsonData)

You’ll see that Xcode omits the data and only provides the number of bytes in jsonData. This output is fine because jsonData contains an unreadable representation of employee1. If you would like to create a readable version of this JSON as a string, you can use theString initializer:

let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
// {"name":"John Appleseed","id":7,"favoriteToy":{"name":"Teddy Bear"}}

Now you can send jsonData or jsonString to the gift department using their special gift API.

If you want to decode the JSON data back into an instance, you need to use JSONDecoder:

let jsonDecoder = JSONDecoder()
let employee2 = try jsonDecoder.decode(Employee.self, from: jsonData)

You must tell the decoder what type to decode with Employee.self.

By design, you specify the type at compile-time as it prevents a security vulnerability where someone on the outside might try to inject a type you weren’t expecting. It also plays well with Swift’s natural preference for static types.

Renaming Properties with CodingKeys

It turns out that the gifts department API requires that the employee ID appear as employeeId instead of id. Luckily, Swift provides a solution to this kind of problem.

CodingKey Protocol and CodingKeys enum

The CodingKeys enum, which conforms to the CodingKey protocol, lets you rename specific properties if the serialized format doesn’t match the API requirements.

Add the nested enumeration CodingKeys like this:

struct Employee: Codable {
  var name: String
  var id: Int
  var favoriteToy: Toy?

  enum CodingKeys: String, CodingKey {
    case id = "employeeId"
    case name
    case favoriteToy
  }
}

There are several things to note here:

  1. CodingKeys is a nested enumeration in your type.
  2. It has to conform to CodingKey.
  3. You also need String as the raw type since the keys must be strings or integers.
  4. You have to include all properties in the enumeration, even if you don’t plan to rename them.
  5. By default, the compiler creates this enumeration, but when you need to rename a key, you must implement it yourself.

If you print the JSON, you’ll see that id has changed to employeeId.

{ "employeeId": 7, "name": "John Appleseed", "favoriteToy": {"name": "Teddy Bear"}}

Manual Encoding and Decoding

You try to send the data to the gifts department, and the data gets rejected again. This time they claim that the information of the gift you want to send to the employee should not be inside a nested type, but rather as a property called gift. So the JSON should look like this:

{ "employeeId": 7, "name": "John Appleseed", "gift": "Teddy Bear" }

In this case, you can’t use CodingKeys since you need to alter the structure of the JSON and not just rename properties. You need to write your own encoding and decoding logic.

The encode Function

As mentioned earlier in the chapter, Codable is just a typealias for the Encodable and Decodable protocols. You need to implement encode(to: Encoder) and describe how to encode each property.

It might sound complicated, but it’s pretty simple. First, update CodingKeys to use the key gift instead of favoriteToy:

enum CodingKeys: String, CodingKey {
  case id = "employeeId"
  case name
  case gift
}

Then, you need to remove Employee’s conformance to Codable and add this extension:

extension Employee: Encodable {
  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)
    try container.encode(favoriteToy?.name, forKey: .gift)
  }
}

First, you get the container of the encoder back, giving you a view into the encoder’s storage that you can access with keys. Note how you choose which properties to encode for which keys. Importantly, you flatten favoriteToy?.name down to the .gift key. If you stop now, you’ll get the following error:

'Employee' does not conform to expected type 'Decodable'

This error is because you removed the conformance to Codable and only added conformance to Encodable. For now, you can comment out the code that decodes jsonString to employee2. If you print jsonString once more, this is what you’ll get:

{"name":"John Appleseed","gift":"Teddy Bear","employeeId":7}

The decode Function

Once the data arrives at the gift department, it must be converted to an instance in the department’s system. Clearly, the gift department needs a decoder. Add the following code to your playground to make Employee conform to Decodable, and thus also Codable:

extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = try values.decode(String.self, forKey: .name)
    id = try values.decode(Int.self, forKey: .id)
    if let gift = try values.decode(String?.self, forKey: .gift) {
      favoriteToy = Toy(name: gift)
    }
  }
}

Here you’re doing the opposite of what you did in the encode method using the decoder’s keyed storage container.

encodeIfPresent and decodeIfPresent

Not all employees have a favorite toy. In this case, the encode method will create a JSON that looks like this:

{"name":"John Appleseed","gift":null,"employeeId":7}

To fix this, you can use encodeIfPresent so the encode method will look like this:

extension Employee: Encodable {
  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)
    try container.encodeIfPresent(favoriteToy?.name, forKey: .gift)
  }
}

With this change, the JSON won’t contain a gift key if the employee doesn’t have a favorite toy.

Next, update the decoder using decodeIfPresent:

extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = try values.decode(String.self, forKey: .name)
    id = try values.decode(Int.self, forKey: .id)
    if let gift = try values.decodeIfPresent(String.self, forKey: .gift) {
      favoriteToy = Toy(name: gift)
    }
  }
}

Writing Tests for the Encoder and Decoder

If you change your encoder and forget to update the decoder (or vice versa), you might get nasty errors at runtime. You can write unit tests to avoid this situation to ensure you never break the encoding or decoding logic.

To do that, you first need to import the XCTest framework. Add this at the top of the playground:

import XCTest

Then you should add a test class and implement the setUp method to initialize a JSONEncoder and JSONDecoder.

Also, initialize one Toy and one Employee instance so you have them ready to use.

Add this at the end of the playground:

class EncoderDecoderTests: XCTestCase {
  var jsonEncoder: JSONEncoder!
  var jsonDecoder: JSONDecoder!
  var toy1: Toy!
  var employee1: Employee!

  override func setUp() {
    super.setUp()
    jsonEncoder = JSONEncoder()
    jsonDecoder = JSONDecoder()
    toy1 = Toy(name: "Teddy Bear")
    employee1 = Employee(name: "John Appleseed", id: 7,
                         favoriteToy: toy1)
  }
}

The next step is to add the tests themselves. Remember that all tests have to start with test.

Add this inside the class EncoderDecoderTests. The contents of the methods should look familiar since it’s mostly a copy of what you previously wrote when you learned how to use encoders and decoders.

func testEncoder() {
  let jsonData = try? jsonEncoder.encode(employee1)
  XCTAssertNotNil(jsonData, "Encoding failed")
  
  let jsonString = String(data: jsonData!, encoding: .utf8)!
  XCTAssert(jsonString.contains("\"employeeId\":7"))
  XCTAssert(jsonString.contains("\"gift\":\"Teddy Bear\""))
  XCTAssert(jsonString.contains("\"name\":\"John Appleseed\""))
}

func testDecoder() {
  let jsonData = try! jsonEncoder.encode(employee1)
  let employee2 = try? jsonDecoder.decode(Employee.self, from: jsonData)
  XCTAssertNotNil(employee2)
  
  XCTAssertEqual(employee1.name, employee2!.name)
  XCTAssertEqual(employee1.id, employee2!.id)
  XCTAssertEqual(employee1.favoriteToy?.name,
                 employee2!.favoriteToy?.name)
}

The most important thing here is the usage of XCTAssert methods. They guarantee the logic is correct and that your encoder and decoder work correctly.

There’s only one thing missing to start using the tests. As explained in Chapter 1: “Access Control, Code Organization & Testing”, for the playground to run the tests, add this at the end of the playground:

EncoderDecoderTests.defaultTestSuite.run()

Once you run the playground, you should see something similar to this:

Test Suite 'EncoderDecoderTests' started at ...
Test Case '-[__lldb_expr_2.EncoderDecoderTests testDecoder]' started.
Test Case '-[__lldb_expr_2.EncoderDecoderTests testDecoder]' passed (0.781 seconds).
Test Case '-[__lldb_expr_2.EncoderDecoderTests testEncoder]' started.
Test Case '-[__lldb_expr_2.EncoderDecoderTests testEncoder]' passed (0.004 seconds).
Test Suite 'EncoderDecoderTests' passed at ...
   Executed 2 tests, with 0 failures (0 unexpected) in 0.785 (0.788) seconds

Challenges

Before moving on, here are some challenges to test your knowledge of encoding, decoding and serialization. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Spaceship

Given the structures below, make the necessary modifications to make Spaceship codable:

struct Spaceship {
  var name: String
  var crew: [CrewMember]
}

struct CrewMember {
  var name: String
  var race: String
}

Challenge 2: Custom Keys

It appears that the spaceship’s interface is different than that of the outpost on Mars. The Mars outpost expects to get the spaceship’s name as spaceship_name. Make the necessary modifications so that encoding the structure would return the JSON in the correct format.

Challenge 3: Write a Decoder

You received a transmission from planet Earth about a new spaceship. Write a custom decoder to convert this JSON into a Spaceship. This is the incoming transmission:

{"spaceship_name":"USS Enterprise", "captain":{"name":"Spock", "race":"Human"}, "officer":{"name": "Worf", "race":"Klingon"}}

Hint: There are no ranks in your type, just an array of crew members, so you’ll need to use different keys for encoding and decoding.

Challenge 4: Decoding Property Lists

You intercepted some weird transmissions from the Klingon, which you can’t decode. Your scientists deduced that these transmissions are encoded with a PropertyListEncoder and that they’re also information about spaceships. Try your luck with decoding this message:

var klingonSpaceship = Spaceship(name: "IKS NEGH’VAR", crew: [])
let klingonMessage = try PropertyListEncoder().encode(klingonSpaceship)

Challenge 5: Enumeration With Associated Values

The compiler can (as of Swift 5.5) automatically generate codable for enumerations with associated values. Check out how it works by encoding and printing out the following list of items.

enum Item {
  case message(String)
  case numbers([Int])
  case mixed(String, [Int])
  case person(name: String)
}

let items: [Item] = [.message("Hi"),
                     .mixed("Things", [1,2]),
                     .person(name: "Kirk"),
                     .message("Bye")]

Key Points

Codable is a powerful tool for saving and loading types. Here are some important takeaways:

  • You need to encode (or serialize) an instance before saving it to a file or sending it over the web.
  • You must decode (or deserialize) to bring it back from a file or the web as an instance.
  • Your type should conform to the Codable protocol to support encoding and decoding.
  • If all stored properties of your type are Codable, then the compiler can automatically implement the requirements of Codable for you.
  • JSON is the most common encoding in modern applications and web services, and you can use JSONEncoder and JSONDecoder to encode and decode your types to and from JSON.
  • Codable is very flexible and can be customized to handle almost any valid JSON.
  • Codable supports serialization formats beyond JSON.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.