An introduction to working with Codable in Swift

Published by donnywals on

Virtually every modern application needs some way to retrieve, and use, data from a remote source. This data is commonly fetched by making a network request to a webserver that returns data in a JSON format.

When you're working with Javascript, this JSON data can be easily decoded into a Javascript object. Javascript doesn't have strong typing, so a JSON object in Javascript is really just a JavaScript Object.

Objects in Javascript are very comparable to dictionaries in Swift, except they aren't strongly typed and they have a couple of extra features. But that's way beyond what I want to cover in this post...

In this post, I want to take a look at Swift's Codable protocol.

So why start with JSON?

Well, JSON is arguably the most common data format that we use to exchange data on the web. And Swift's Codable protocol was designed to provide a powerful and useful mechanism to convert JSON data into Swift structs.

What's nice about Codable is that it was designed to not be limited to JSON. Out of the box, Codable can also be used to decode a .plist file into Swift structs, or to convert Swift structs into data for a .plist file.

The post you're looking at is intended to provide an introduction into Swift's Codable protocol, and it's part of a series of posts on this topic. I will focus on showing you how to work with JSON and Codable in Swift. It's good to understand that the principles in this series can be applied to both JSON data, as well as .plist files.

I'll start by explaining what Swift's Codable is. After that, I'll show you how to define a struct that implements the Codable protocol, and I'll explain the basics of encoding and decoding JSON data.

Understanding what Swift's Codable is

The Codable protocol in Swift is really a union of two protocols: Encodable and Decodable. These two protocols are used to indicate whether a certain struct, enum, or class, can be encoded into JSON data, or materialized from JSON data.

When you only want to convert JSON data into a struct, you can conform your object to Decodable. If you only want to transform instances of your struct into Data, you can conform your object to Encodable, and if you want to do both you can conform to Codable.

A lot of Swift's built-in types already conform to Codable by default. For example, Int, String, and Bool are Codable out of the box.

Even dictionaries and arrays are Codable by default as long as the objects that you store in them conform to Codable.

This means that an array defined as Array<String> conforms to Codable already. A dictionary that's defined as Dictionary<String: String> is Codable too.

Arrays and dictionaries both play important roles in JSON because everything in JSON is defined using the equivalent of Swift's arrays and dictionaries.

For example, the following is valid JSON for an array of strings:

["hello", "world"]

And the following is an example of a dictionary in JSON:

{
    "hello": "world",
    "someInt": 10,
    "someBool": true
}

Notice how this dictionary has String as its key and three different kinds of values as its value. In Swift, you might represent a dictionary like this as [String: Any]. If we want to decode this JSON into something useful, we can't use [String: Any]. Because Any isn't Codable, a dictionary that has Any as its key can't be Codable either.

Luckily, all values for this object are Codable. Remember, Swift's String, Int, and Bool are all Codable!

Earlier I wrote that your structs, enums, and classes can conform to Codable. Swift can generate the code needed to extract data to populate a struct's properties from JSON data as long as all properties conform to Codable.

In this case, that means we would define a struct that has three properties with types String, Int, and Bool. Swift will take care of the rest.

Let's take a look at an example.

Defining a Codable struct

Given a specific JSON object, it's possible for us to figure out and define structs, classes, and enums that represent this JSON data in Swift.

The easiest way to do this, is to mirror the JSON structure 1-on-1. In this post, you will learn how you can customize the mapping between your Codable object an the JSON data you want to encode or decode. In this post, you will learn how to write custom logic to extract JSON data for a struct that's completely different from the JSON data that's used to populate the struct. For now, we'll focus on a direct mirror.

Earlier, I showed you this JSON:

{
    "hello": "world",
    "someInt": 10,
    "someBool": true
}

If we'd model this data using a Swift struct, we'd write the following:

struct ExampleStruct: Decodable {
    let hello: String
    let someInt: Int
    let someBool: Bool
}

Notice how I declared my struct as ExampleStruct: Decodable. This means that my struct conforms to Decodable, and I can decode JSON into instances of this struct. If I'd want to encode instances of my struct into JSON data, I would declare my struct as ExampleStruct: Encodable, and to convert in both directions I'd use ExampleStruct: Codable.

In this case, I only want to decode so I'm declaring my struct as Decodable.

Notice how the property names for my struct exactly match the keys in my JSON dictionary. This is important because the code that Swift generates behind the scenes for you when you compile your code assumes that the keys in your JSON match the property names of your Decodable object.

The properties of my struct are all Decodable themselves, this means that Swift can automatically generate the code needed to decode JSON data into my struct.

Let's take a look at a more complex JSON structure:

{
  "status": "active",
  "objects": [
    {
      "id": 1,
      "name": "Object one",
      "available": true
    },
    {
      "id": 2,
      "name": "Object two",
      "available": false
    },
  ]
}

In this example, we have a JSON object with two keys, one of them has an array as its value as you can tell by the [] that wrap the value for objects. The array contains more JSON objects. JSON objects are always wrapped by {}.

If we look at this JSON data from the point of view of our struct, we can see that we should define one struct with two properties (status and objects), and that objects should be an array of sorts. This array will hold instances of another struct that has three properties (id, name, and available).

Here's what our Swift models might look like:

struct Response: Decodable {
  let status: String
  let objects: [Product]
}

struct Product: Decodable {
  let id: Int
  let name: String
  let available: Bool
}

Swift can generate code to decode JSON into these structs because Product's properties are all Decodable. This means that Response's properties are also all Decodable since [Product] is Decodable. Remember, arrays are Decodable (or Codable) as long as their Element is Decodable (or Codable).

What's interesting about Codable, is that we can also make enums Codable, as long as they have a raw value that is Codable. For example, we could change the Response's status property to a ResponseStatus enum as follows:

struct Response: Decodable {
  let status: ResponseStatus
  let objects: [Product]
}

enum ResponseStatus: String, Decodable {
  case active = "active"
  case inactive = "inactive"
}

When we attempt to decode our JSON data into Response, the decoding will fail if we receive an unkown value for ResponseStatus.

Depending on your use case, this might be desired, or a problem. In this post, you'll learn how you can write custom decoding logic that will allow you to decode unkown values into a special other case that has an associated value (case other(String)) that can be used to represent new and unkown enum cases for a Decodable enum.

Now that you've seen some examples of how you can define a Decodable struct, let's see how you can decode JSON data into a Decodable struct with a JSONDecoder.

Decoding JSON into a struct

When you've obtained a Data object that represents JSON data, you'll want to decode this data into your Swift struct (or class of course). If you don't have a remote API to practice with, you can define some dummy JSON data using Swift's multiline string syntax as follows:

let exampleData = """
{
  "status": "active",
  "objects": [
    {
      "id": 1,
      "name": "Object one",
      "available": true
    },
    {
      "id": 2,
      "name": "Object two",
      "available": false
    },
  ]
}
""".data(using: .utf8)!

You can call data(using:) on any Swift string to obtain a data representation for that string.

To convert your Data to an instance of your struct, you need a JSONDecoder instance. You can create one as follows:

let decoder = JSONDecoder()

To decode the dummy data I showed you just now into an instance of the Response struct from the previous section, you'd use the following code:

do {
  let jsonDecoder = JSONDecoder()
  let decodedResponse = try jsonDecoder.decode(Response.self,
                                               from: exampleData)

  print(decodedResponse)
} catch {
  print(error)
}

Your JSONDecoder instance has a decode(_:from:) method that you call to convert JSON data into the object of your choosing.

The first argument for this method is the type that you want to decode your data into. In this case, that's Response.self.

The second argument for this method is the data that you want to extract your data from. In this case, that's exampleData.

Because JSON decoding can fail, decode(_:from:) must be called with a try prefix, preferably in a do {} catch {} block.

If something goes wrong we print the error so we can see what went wrong. The error messages that are surfaced by JSONDecoder are generally very helpful. For example, if our struct would contain a type that is not present in the JSON data we would see an error that looks like this:

keyNotFound(CodingKeys(stringValue: "missingObject", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"missingObject\", intValue: nil) (\"missingObject\").", underlyingError: nil))

We can see that we're dealing with a keyNotFound error. We can find out which key wasn't found by reading the CodingKeys declaration that comes after the error case. In this case, the CodingKeys value tells us that we're trying to extract a value for the missingObject key but that key does not exist in the JSON as noted by the debugDescription.

When you see an error like this it usually means that you made a typo, or your JSON object doesn't always contain a specific key. This can happen when your remote data source doesn't include keys with a nil value.

If you made a typo, you should fix it. If your remote data source omits keys with a nil value, you can mark your property as optional. That way the missing property will get a nil value automatically if it's missing in the JSON response.

All errors you might encounter when decoding JSON in Swift follow a similar pattern. Make sure you read your decoding errors if you encounter them because they'll typically provide you with very useful information to debug and fix your models.

Now that you've seen how to decode data, let's take a look at doing the opposite; encoding structs into JSON data.

Encoding a struct to JSON

When you encode data from a struct, class, or enum to JSON data, the end result of your encoding will always be Data. In other words, you decode Data into Decodable objects, and you encode an Encodable object into Data. This data can be written to a file, sent to a server, it could even be persisted using a Core Data entity or UserDefaults. However, the most common goal when encoding objects is to either write the data to a file, or to send it to a server.

Take a look at the following Encodable struct:

struct Product: Codable {
    let id: Int
    let name: String
    let available: Bool
}

Now let's see how you can encode an instance of this struct to Data:

let sampleInput = Product(id: 0, name: "test name", available: true)

do {
  let encoder = JSONEncoder()
  let data = try encoder.encode(sampleInput)
  print(data)
} catch {
  print(error)
}

This code is pretty straightforward, and if you run this in a playground, you'll find that the printed output is the following:

44 bytes

That might be surprising to you. After all, you encoded your struct to JSON data, right?

Well, you did. But Data is data and it's represented as bytes. You can inspect the generated JSON by transforming the data to a string:

if let jsonString = String(data: data, encoding: .utf8) {
  print(jsonString)
}

The output for this code is the following:

{"id":0,"name":"test name","available":true}

Neat! That's a nice JSON string.

Note that this output is not what you should typically send to a server or write to a file. Instead, you should use the Data that was returned by the JSON encoder's encode method. That Data is the binary representation of the String that we just printed.

By default, JSONEncoder will encode your objects into a single-line JSON structure like you just saw. The exampleData that I showed you earlier was nicely formatted on multiple lines. It's possible to configure JSONEncoder to insert newlines and tabs into the output, this allows you to inspect a nicely formatted string representation of the JSON data. you can do this by setting the encoder's outputFormatting to .prettyPrinted:

do {
  let encoder = JSONEncoder()

  encoder.outputFormatting = .prettyPrinted

  let data = try encoder.encode(sampleInput)
  if let jsonString = String(data: data, encoding: .utf8) {
    print(jsonString)
  }
} catch {
  print(error)
}

The output for the code below would look like this:

{
  "id" : 0,
  "name" : "test name",
  "available" : true
}

If you're inspecting a large JSON structure, it's nice to use this pretty printed format. It's not common to need this output format when you write your encoded data to a file, or when you send it to a server. The whitespace is only useful for humans, and it doesn't provide any value to machines that interpret the JSON data.

A more important outputFormatting is .sortedKeys. When you set the output formatting to .sortedKeys, the generated Data will have your JSON keys sorted alphabetically. This can be useful if your server expects you to format your keys in a specific way, or if you want to compare to different encoded objects to see if their data is the same. If the keys aren't sorted, two Data instances that hold the same JSON data might not be equal due to differences in how their keys are ordered.

Here's an example of the encoded sampleInput from earlier when using a JSONEncoder that has its outputFormatting set to .sortedKeys:

{"available":true,"id":0,"name":"test name"}

The output isn't pretty printed but notice how the encoded keys are now in alphabetical order.

It's not common to have to encode your JSON data using a specific key sorting, but it's good to know this option exists if needed. I know I've needed it a few times when working with third party APIs that had requirements about how the JSON data I sent it was formatted.

You can combine the .sortedKeys and .prettyPrinted options by setting outputFormatting to an array:

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

In Summary

In this post, you learned everything you need to know to get started with JSON encoding and decoding in Swift. You learned what the Codable protocol is, you learned how Swift automatically generates encoding and decoding logic for objects that conform to Codable, and you learned that Codable is really a union of two protocols; Encodable and Decodable.

I also showed you several examples of decoding JSON into Swift objects, and of encoding Swift objects into JSON.

In future posts, we'll dive deeper into thinks like CodingKeys, custom encoding- and decoding logic, and more advanced examples of how you can work with complex JSON data.


Stay up to date with my weekly newsletter





Marketing permission: I give my consent to donnywals.com to be in touch with me via email using the information I have provided in this form for the purpose of receiving a weekly newsletter.



Practical Combine

Learn everything you need to know about Combine and how you can use it in your projects with my new book Practical Combine. You'll get thirteen chapters, a Playground and a handful of sample projects to help you get up and running with Combine as soon as possible.

The book is available as a digital download for just $34.99!

Get Practical Combine
Categories: Codable