Working With Codable And JSON In Swift

Written by Reinder de Vries on August 11 2019 in App Development

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, simply by 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 JSON data to Swift objects effectively?

In this article 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. Getting Started: Encoding & Decoding
  2. The Codable Protocol Explained
  3. Decoding JSON To Swift Objects With Codable
  4. Encoding Swift Objects As JSON With Codable
  5. Working With Nested Arrays And Codable
  6. Further Reading

Getting Started: Encoding & 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:

{
    "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!"
}

JSON objects are wrapped in squiggly brackets { and }, arrays are wrapped in square brackets [ and ]. Property names are strings, wrapped in quotes. And values can be strings, numbers (no quotes), arrays or other objects.

JSON is a text-based data format that many webservices use, including APIs from Twitter, Facebook, Foursquare. It’s superior to XML, 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 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
}

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 centerpiece of this workflow is Swift’s Codable protocol. Let’s find out how it works, next!

Learn how to build iOS apps

Get started with iOS 12 and Swift 5

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 5 and Xcode 10.

The Codable Protocol Explained

Using JSON before Swift 4 was a bit of a PITA. You’d have to serialize the JSON yourself, 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 now use the Codable protocol. Your Swift struct or class merely has to adopt that protocol, and you get JSON encoding and decoding for free.

The Codable protocol is a composition of two protocols, Decodable and Encodable. Both protocols are pretty minimal, and only define the functions init(from: Decoder) and encode(to: Encoder) respectively. In other words, 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. The JSONDecoder and JSONEncoder classes use those two protocols to provide the functionality to decode/encode JSON.

In essence, the JSONDecoder class contains code to convert the JSON format to a key-value container. Thanks to composition, you can create your own encoder and decoder for any imaginable format.

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, 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. Awesome!

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

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.

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.

What if our JSON properties, like first_name, 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.

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 is self-explanatory. It simply maps cases to properties, and uses the string values to select the right property names in the JSON data.

Encoding Swift Objects As JSON With Codable

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 there?

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

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!

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.

In JSON, arrays are denoted by square brackets [ ], and objects by squiggly brackets { }. 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], or “array of User objects”. Instead of working with one User object, we want to decode a bunch of them.

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.

Here, check this out:

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

    var users:[User]
}

Here’s what happened:

  • 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.
  • The Response struct has one property called users of type [User], or array of User objects.

The cool thing is that semantically, both data structures are exactly the same. 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)
}

The type that JSONDecoder needs to decode is Response.self. It’ll automatically pick up on the nested User type. And on the last line, we’re directly iterating the users array on response. 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.

Learn how to build iOS apps

Get started with iOS 12 and Swift 5

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 5 and Xcode 10.

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:

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.