Chapters

Hide chapters

Swift Apprentice

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 10 chapters
Show chapters Hide chapters

22. Encoding & Decoding Types
Written by Eli Ganim

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

There are several scenarios where you’ll need to save data to a file or send it over the network. In this chapter, you’ll learn how to achieve these tasks by converting your instances to another representation, like a string or a stream of bytes. 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:

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:

Encodable and Decodable protocols

The Encodable protocol is used by types that can be encoded to another representation. It declares a single method:

func encode(to: Encoder) throws

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

The Decodable protocol is used by types that can be decoded. It declares just a single initializer:

init(from decoder: Decoder) throws

You will know when and how to implement these methods by the end of this chapter.

What is Codable?

Codable is a protocol that a type can conform to, to declare that it can be encoded and decoded. It’s basically an alias for the Encodable and Decodable protocols.

typealias Codable = Encodable & Decodable

Automatic encoding and decoding

There are many types in Swift that 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 to do it is by conforming to Codable and making sure all its stored properties are also codable.

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

struct Toy: Codable {
  var name: String
}

Encoding and decoding custom types

There are several representations you can encode to or decode from, such as XML or a Property List. In this section, you’ll learn how to encode to and decode from JSON, by using Swift’s JSONEncoder and JSONDecoder classes.

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

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)
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(employee1)
print(jsonData)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
// {"name":"John Appleseed","id":7,"favoriteToy":{"name":"Teddy Bear"}}
let jsonDecoder = JSONDecoder()
let employee2 = try jsonDecoder.decode(Employee.self, from: jsonData)

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 CodingKey protocol, lets you rename specific properties in case the serialized format doesn’t match the requirements of the API.

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

  enum CodingKeys: String, CodingKey {
    case id = "employeeId"
    case name
    case favoriteToy
  }
}
{ "employeeId": 7, "name": "John Appleseed", "favoriteToy": {"name": "Teddy Bear"}}

Manual encoding and decoding

You try to send the data over to the gifts department, and again the data gets rejected. 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 actually look like this:

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

The encode function

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

enum CodingKeys: String, CodingKey {
  case id = "employeeId"
  case name
  case gift
}
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)
  }
}
'Employee' does not conform to expected type 'Decodable'
{"name":"John Appleseed","gift":"Teddy Bear","employeeId":7}

The decode function

Once the data arrives at the gift department, it needs to 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)
    }
  }
}

encodeIfPresent and decodeIfPresent

It turns out 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}
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)
  }
}
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 at any time you change your encoder and forget to update the decoder (or vice versa) you might get nasty errors at runtime. In order to avoid this situation, it’s recommended that you write unit tests to make sure you never break the encoding or decoding logic.

import XCTest
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)
  }
}
func testEncoder() {
  let jsonData = try? jsonEncoder.encode(employee1)
  XCTAssertNotNil(jsonData, "Encoding failed")
  
  let jsonString = String(data: jsonData!, encoding: .utf8)!
  XCTAssertEqual(jsonString, "{\"name\":\"John Appleseed\",\"gift\":\"Teddy Bear\",\"employeeId\":7}")
}

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)
}
EncoderDecoderTests.defaultTestSuite.run()
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 if you 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: [Spaceman]
}

struct Spaceman {
  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"}}

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)

Key points

  • You need to encode (or serialize) an instance before you can save it to a file or send it over the web.
  • You need to 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 can be used with 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.
© 2023 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now