Home iOS & Swift Books Swift Apprentice

22
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. In this chapter, you’ll learn 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:

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 expresses that a type can convert itself into another representation. It declares a single method:

func encode(to: Encoder) throws

The compiler generates this automatically 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 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 of the stored properties conform to Decodable. You will know when and how to implement these methods yourself 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 an alias for the Encodable and Decodable protocols. Literally:

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 if the serialized format doesn’t match the API requirements.

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 look like this:

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

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.

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 you change your encoder and forget to update the decoder (or vice versa), you might get nasty errors at runtime. To avoid this situation, you can write unit tests to ensure 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 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"}}

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 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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.