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

4.6 (54) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. Add the relevant coding keys.
  2. Decode the properties specific to the subclass.
  3. Use superDecoder(forKey:) to get a decoder instance suitable to pass to the init(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:

  1. Define enough coding keys to cover all possible cases.
  2. 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.

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:

  1. Get a keyed container as usual, then inspect the allKeys property to determine which keys were present in the JSON.
  2. 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.
  3. 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:

  1. Get the decoder’s unkeyed decoding container and loop through it with decode(_:) to decode the final, correctly-formatted label name.
  2. Decode labelData to Label 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!