Working with Codable and JSON in Swift

Written by LearnAppMaking on January 20 2021 in App Development, iOS, Swift

Working with Codable and JSON in Swift

You can use Codable in Swift to encode and decode custom data formats, such as JSON, to native Swift objects. It’s incredibly easy to map Swift objects to JSON data, and vice versa, by simply adopting the Codable protocol.

As a pragmatic iOS developer, you’ll come across JSON sooner rather than later. Every webservice, from Facebook to Foursquare, uses the JSON format to get data into and out of your app. How can you encode and decode that JSON data to Swift objects effectively?

In this tutorial you’ll learn how to work with JSON objects in Swift, by using the Codable protocol. What you’ll learn, can be easily applied to other data formats as well. We’ll get into encoding and decoding with JSONEncoder and JSONDecoder, and I’ll show you how to mediate between JSON and Swift data.

Ready? Let’s go.

  1. Get Started: Encoding and Decoding
  2. The Codable Protocol Explained
  3. Working with Codable
  4. Decoding JSON to Swift Objects with Codable
  5. Encoding Swift Objects as JSON with Codable
  6. Working with Nested Arrays and Codable
  7. Further Reading

Get Started: Encoding and Decoding

What problem does the Codable protocol in Swift actually solve? Let’s start with an example.

Imagine you’re building a recipe app. The app shows various recipes in a list, including the ingredients, instructions and basic information about food.

You get the data for the app from a webservice, and their cloud-based API. This API uses the JSON data format. Every time you request a recipe from the webservice, you get JSON data back.

Here’s an example of JSON data for a recipe:

{
    "name":     "Spaghetti Bolognese",
    "author":   "Reinder's Cooking Corner",
    "url":      "https://cookingcorner.com/spaghetti-bolognese",
    "yield":    4,
    "ingredients": ["cheese", "love", "spaghetti", "beef", "tomatoes", "garlic"],
    "instructions": "Cook spaghetti, fry beef and garlic, add tomatoes, add love, eat!"
}

Take a look at the structure of the JSON data.

JSON objects are wrapped in squiggly brackets { and }, and arrays are wrapped in square brackets [ and ]. Property names are strings, wrapped in quotes ". Values in JSON can be strings, numbers (no quotes), arrays or other objects. You can also nest data, i.e. arrays in arrays, objects in arrays, etc., to create a complex hierarchy of data.

JSON is a text-based data format that many webservices use, including APIs from Twitter, Facebook, Foursquare, and so on. If you’re building apps that use web-based resources, you will run into JSON.

The JSON format is superior to XML, a common alternative, because it’s efficient, easily parsed, and readable by humans. JSON is an agreed upon format for webservices, APIs and apps. It’s used throughout the web, apps, and online services, because the format is simple and flexible.

And JSON has one superb capability: you can encode any data format in JSON, and decode JSON back to any data format. This encoding and decoding process is what makes JSON so powerful.

You can take your Swift String, Int, Double, URL, Date, Data, Array and Dictionary values, and encode them as JSON. You then send them to the webservice, which decodes the values into a native format that it understands. Similarly, the webservice sends data encoded as JSON to your app, and you decode the data to native types such as String, Double and Array.

When your recipe app receives the JSON (see above), it can then be decoded into a Swift struct, like this one:

struct Recipe {
    var name: String
    var author: String
    var url: URL
    var yield: Int
    var ingredients: [String]
    var instructions: String
}

In Swift, the Codable protocol is used to go from a JSON data object to an actual Swift class or struct. This is called decoding, because the JSON data is decoded into a format that Swift understands. It also works the other way: encoding Swift objects as JSON.

The Codable protocol in Swift is actually an alias of the Decodable and Encodable protocols. Because you often use encoding and decoding together, you use the Codable protocol to get both protocols in one go.

The centerpiece of the encoding/decoding workflow is Swift’s Codable protocol. Let’s find out how it works, next!

Can’t tell “encoding” and “decoding” apart? Think of it like this: We’re converting data from and to “code”, like an Enigma machine or secret crypto cipher. Encoding means converting data to code; en-coding, or “in/within code”. Decoding means converting code to data; de-coding, or “from/off code”.

The Codable Protocol Explained

Using JSON before Swift 4 was a bit of a PITA. You’d have to serialize the JSON yourself with JSONSerialization, and then type cast every property of the JSON to the right Swift type.

Like this:

let json = try? JSONSerialization.jsonObject(with: data, options: [])

if let recipe = json as? [String: Any] {
    if let yield = recipe["yield"] as? Int {
        recipeObject.yield = yield
    }
}

The above snippet only grabs a yield value from the JSON, and casts it to Int. It’s extremely verbose, and it’s hard to properly respond to potential errors and type discrepancies. Even though it works, it’s not ideal.

Libraries like SwiftyJSON make working with JSON a lot easier, but you still need to map the JSON data to their respective Swift objects and properties.

With Swift 4, and later, you can use the Codable protocol instead. Your Swift struct or class merely has to adopt the Codable protocol, and you get JSON encoding and decoding out of the box, for free.

The Codable protocol is a composition of two protocols, Decodable and Encodable. Both protocols are pretty minimal; they only define the functions init(from: Decoder) and encode(to: Encoder), respectively. In other words, these functions mean that for a type to be “decodable” or “encodable”, they’ll need to “decode from something” and “encode to something”.

The real magic of Codable happens with the Decoder and Encoder protocols. These protocols are adopted by the components that encode/decode various data formats, like JSON. In Swift, we’ve got a few -coders:

  • PropertyListEncoder and PropertyListDecoder for .plist property lists
  • JSONEncoder and JSONDecoder for JSON – that’s us!
  • NSKeyedArchiver can work with Codable, via PropertyListEncoder, Data and NSCoding

The JSONDecoder and JSONEncoder classes use those the Decoder and Encoder protocols to provide the functionality to decode/encode JSON. That also means you can write your own custom encoder/decoder for Codable, provided you adopt the Decoder and Encoder protocols!

Need a refresher on protocols in Swift? Read Protocols In Swift Explained to catch up.

Working with Codable

Let’s take a look at an example. We’re going to map some JSON data to a Swift struct. We’re essentially decoding the JSON to an actual Swift object.

First, we’re creating a struct called User. Like this:

struct User: Codable {
    var first_name: String
    var last_name: String
    var country: String
}

The User struct has three simple properties of type String, and conforms to the Codable protocol.

Then, let’s write a bit of JSON. This is the JSON we’ll work with:

{
    "first_name": "John", 
    "last_name":  "Doe", 
    "country":    "United Kingdom"
}

JSON data typically enters your app as the response of a webservice request, or through a .json file, but for this example we simply put the JSON in a String. Like this:

let jsonString = """
{
    "first_name": "John",
    "last_name":  "Doe",
    "country":    "United Kingdom"
}
"""

Note: The above code uses the triple quote """ to create multi-line strings. Neat!

What we’re doing next, is decoding the JSON and turning it into a User object. Like this:

let jsonData = jsonString.data(using: .utf8)!
let user = try! JSONDecoder().decode(User.self, from: jsonData)
print(user.last_name)
// Output: Doe

Here’s what happens:

  • First, you turn jsonString into a Data object by calling the data(using:) function on the string. This is a necessary intermediate step.
  • Then, you create a JSONDecoder object and immediately call the decode(_:from:) function on it. This turns the jsonData into an object of type User, by decoding the JSON. The User.self type is provided as a parameter.
  • Finally, you print out the last name of the user with print(user.last_name). That user value has User as its type, so it’s an actual Swift object!

Easy, right? You’ve essentially “mapped” the JSON object to a Swift struct, and decoded the JSON format to a native object Swift can work with.

In the above code, we’re ignoring any errors thrown by decode(_:from:) with try!. In your own code, you’ll need to handle errors with a do-try-catch block. An error that may be thrown, for example, comes from providing invalid JSON.

The User.self is a metatype, a reference to the type User itself. We’re telling the decoder that we want to decode the data with the User struct, by providing it a reference to that type.

A common error when defining your Codable type, like the User struct, is mismatched keys and properties. If you’ve added a property to your struct, that isn’t present in the JSON or has a different type, you’ll get an error when decoding the JSON. The opposite, i.e. a property that doesn’t exist in your struct but does in the JSON, is not a problem. It’s easiest to debug this one property at a time, i.e. keep adding properties until the JSON doesn’t decode without errors anymore. You can also determine what properties/keys to include or ignore with the CodingKeys enum (see below).

Decoding JSON to Swift Objects with Codable

Let’s take another look at the example from the previous section, and expand it. Here’s the JSON we’re working with:

let jsonString = """
{
    "first_name": "John",
    "last_name":  "Doe",
    "country":    "United Kingdom"
}
"""

We then turn that into a Data object. Like this:

let jsonData = jsonString.data(using: .utf8)!

This is a necessary intermediate step. Instead of representing the JSON as a string, we now store the JSON as a native Data object. The character encoding we’re using for that string is UTF8.

If you look closely, you’ll see that the above code uses force unwrapping to work with the optional return value from data(using:). Let’s unwrap that optional more elegantly!

if let jsonData = jsonString.data(using: .utf8) 
{
    // Use `jsonData`
} else {
    // Respond to error  
}

With the above code we can respond to errors when data(using:) returns nil. You could for instance show an error message, or silently let the task fail and save diagnostic information in a log.

Next, we’re creating a new JSONDecoder object. Like this:

let decoder = JSONDecoder()

We then use that decoder to decode the JSON data. Like this:

let user = try! decoder.decode(User.self, from: jsonData)

However, the decode(_:from:) function can throw errors. The above code crashes whenever that happens. Again, we want to respond to any errors that might happen. Like this:

do {
    let user = try decoder.decode(User.self, from: jsonData)
    print(user.last_name)
} catch {
    print(error.localizedDescription)
}

And the entire code block now looks like the following. See how that’s different?

if let jsonData = jsonString.data(using: .utf8)
{
    let decoder = JSONDecoder()

    do {
        let user = try decoder.decode(User.self, from: jsonData)
        print(user.last_name)
    } catch {
        print(error.localizedDescription)
    }
}

Never silence errors. Catch the error and respond to it, either with UI/UX, by retrying the task, or by logging, crashing and fixing it.

Working with CodingKeys

What if our JSON properties, like first_name and/or firstName, are different than the Swift struct properties? That’s where CodingKeys comes in.

Every class or struct that conforms to Codable can declare a special nested enumeration called CodingKeys. You use it to define properties that should be encoded and decoded, including their names.

Let’s take a look at an example. In the User struct below, we’ve changed the property names from snake_case to camelCase. For example, the key first_name is now called firstName.

struct User:Codable 
{
    var firstName: String
    var lastName: String
    var country: String

    enum CodingKeys: String, CodingKey {
        case firstName = "first_name"
        case lastName = "last_name"
        case country
    }
}

When you use the above struct with the examples in the previous sections, you’ll see that you can use a User object with the new property names. Like this:

print(user.firstName)
// Output: John

print(user.country)
// Output: United Kingdom

The CodingKeys enumeration maps its cases to properties, and uses the string values to select the right property names in the JSON data. The case in the enum is the name of the property you want to use in the struct, and its value is the name of the key in the JSON data.

Encoding Swift Objects as JSON with Codable

So far we’ve focused on decoding JSON data to Swift objects. What about going the other way? Can we also encode objects as JSON? Yes, why not!

Like this:

var user = User()
user.firstName = "Bob"
user.lastName = "and Alice"
user.country = "Cryptoland"

let jsonData = try! JSONEncoder().encode(user)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)

And the output is:

{"country":"Cryptoland","first_name":"Bob","last_name":"and Alice"}

What happens here?

  • First, you create a User object and assign some values to its properties
  • Then, you use encode(_:) to encode the user object to a JSON Data object
  • Finally, you convert the Data object to String and print it out

If you look closely, you’ll see that encoding follows the same steps as decoding, except in reverse. Going from Swift object, through the decoder, resulting in a JSON string.

Just as before, can we expand the example to deal with errors? Yes! Like this:

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

do {
    let jsonData = try encoder.encode(user)

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

In the above example, we’re also using the outputFormatting property of the encoder to “pretty print” the JSON data. This adds spaces, tabs and newlines to make the JSON string easier to read. Like this:

{
    "country" : "Cryptoland",
    "first_name" : "Bob",
    "last_name" : "and Alice"
}

Awesome!

It’s worth noting that dictionaries in Swift, and in JSON, do not have a fixed sorting order. That’s why you’ll see varying sort orders for the country, first_name, etc. keys in the JSON, if you run the above code a few times. Most webservices that output JSON will enforce a fixed sorting order through a schema, which definitely makes it easier to read and navigate the JSON.

Working with Nested Arrays and Codable

So far we’ve worked with simple JSON objects – just a class with a few properties. But what if the data you’re working with is more complex?

For example, the JSON data you may get back from a Twitter or Facebook webservice is often nested. That is, the data you want to get to is buried in 2-3 levels of JSON arrays and dictionaries. What now!?

Let’s look at a simple example, first. Here’s the Swift struct we’ll work with:

struct User: Codable {
    var first_name: String
    var last_name: String
}

Then, here’s the JSON data we want to decode:

let jsonString = """
[
    {
        "first_name": "Arthur",
        "last_name": "Dent"
    }, {
        "first_name": "Zaphod",
        "last_name": "Beeblebrox"
    }, {
        "first_name": "Marvin",
        "last_name": "The Paranoid Android"
    }
]
"""

If you look closely, you see that the data we need is stored as an array. The JSON isn’t one simple object, but it’s an array with 3 User objects. We know it’s an array of objects, because of the square brackets [ ] (array) and squiggly brackets { } (object). Multiple items are always separated by commas.

How can we decode these User objects? Here’s how:

let jsonData = jsonString.data(using: .utf8)!
let users = try! JSONDecoder().decode([User].self, from: jsonData)

for user in users {
    print(user.first_name)
}

The above code snippet is extremely similar to what you’ve seen before, but there’s one important difference. See the type we’re providing to the decode(_:from:) function? The type is [User] (with .self), or “array of User objects”. Instead of working with one User object, we want to decode a bunch of them, and that’s designated with the [User] array type.

Next up, we’re going to discuss how you can work with more complex nested types. Consider, for example, that the array of User objects is nested inside a JSON dictionary. Like this:

let jsonString = """
{
    "users":
    [
        {
            "first_name": "Arthur",
            "last_name": "Dent"
        }, {
            "first_name": "Zaphod",
            "last_name": "Beeblebrox"
        }, {
            "first_name": "Marvin",
            "last_name": "The Paranoid Android"
        }
    ]
}
"""

In the above JSON snippet, the top-level element is a dictionary (or “object”). It only has one key-value pair: a users array. The data we want is inside that array. How do we get to it?

It’s important to make our approach not too complicated. It’s easy to think you’re going to need some advanced JSON parsing, but as it turns out, we can actually also nest Swift structs. We’re going to describe the entire JSON as a struct, with the User struct inside it.

Here, check this out:

struct Response: Codable
{
    struct User: Codable {
        var first_name: String
        var last_name: String
    }

    var users: [User]
}

Here’s what’s going on:

  • The top-level JSON dictionary corresponds to the Response type. Just as before, this type conforms to Codable, to support JSON encoding and decoding.
  • Inside that struct we’ve defined another struct called User. This is the exact same type we’ve used before. This struct is “nested”.
  • The Response struct has one property called users of type [User], or array of User objects.

The cool thing is that semantically, both the JSON and Swift data structures are exactly the same. They just use a different syntax. We’ve defined a nested array inside the top-level dictionary, just like the Response struct has a nested User struct and users property inside it.

Making it work is a piece of cake now. Check this out:

let jsonData = jsonString.data(using: .utf8)!
let response = try! JSONDecoder().decode(Response.self, from: jsonData)

for user in response.users {
    print(user.first_name)
}

See how we’re using the Response type to decode the JSON, and then loop over the response.users property? Check that with the Response struct, and the JSON. We’re picking off the users key-value pair in the top-level dictionary, and map the objects inside to User objects. Neat!

Using nested types is a great general-purpose approach to complex JSON data structures. When you come across a bit of JSON you can’t easily represent in a tangible type, like User or Tweet, try to expand the type to something like Response or UserCollection. Instead of going deeper, go wider, and incorporate the complete JSON. Use nested types to get to the data you want. It’s also worth noting here that you don’t have to add the User struct in the Response struct – it can go outside of it, too.

Further Reading

And that’s all there is to it! You now know:

  • How to use Codable to create objects that can be encoded and decoded
  • How to use JSONDecoder and JSONEncoder to encode and decode JSON objects and their Swift counterparts
  • What encoding and decoding is for, and why it’s important in everyday iOS development
  • How to work with more complex JSON data, and how to use nested types

Want to learn more? Check out these resources:

LearnAppMaking

LearnAppMaking

At LearnAppMaking.com, app developers learn how to build and launch awesome iOS apps.