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 2 of 4 of this article. Click here to view the first page.

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 structs. 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(favoriteToy.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:

  1. Create a set of coding keys to represent your JSON fields. Because you’re not doing any mapping, you don’t need to declare them as strings since there are no raw values.
  2. Create a KeyedEncodingContainer. This is like a dictionary you can store your properties in as you encode them.
  3. Encode the name and id properties directly to the container.
  4. Encode the toy’s name directly into the container, using the gift key.

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:

  1. Get a keyed container from the decoder, this will contain all of the properties in the JSON.
  2. Extract the name and id values from the container using the appropriate type and coding key.
  3. Extract the gift’s name, and use that to build a Toy and assign it to the correct property.

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(favoriteToy, forKey: .toy)
}

This is how the above code works:

  1. Create your top-level coding keys.
  2. Create another set of coding keys, which you’ll use to create another container.
  3. Encode the name and id the way you’re used to.
  4. Create a nested container nestedContainer(keyedBy:forKey:) and encode favoriteToy 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:

  1. 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.
  2. Set dateEncodingStrategy and dateDecodingStrategy 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.