Back to blog

Working with Codable and JSON in Swift


Abhinav Girdhar
By Abhinav Girdhar | Last Updated on December 20th, 2023 11:44 am | 6-min read

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 app development 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.

Describe your app idea
and AI will build your App

 

 

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.

Now, think of JSON as a universal language that both the webservice and your app understand. It’s like when two people from different countries use English as a common language to communicate.

In the world of apps and web services, JSON serves a similar purpose. But how do you translate this JSON language into something your app can work with directly? That’s where the Codable protocol shines.

It acts as a translator, converting the JSON data into Swift objects and vice versa, making it easier for developers to handle and manipulate the data within the app.

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.

To visualize this, think of the older approach as manually translating a book word by word using a dictionary. It’s tedious, error-prone, and not very efficient.

Each time you encounter a word (or in this case, a piece of data), you’d have to look it up, understand its context, and then translate it. Mistakes can easily creep in, and the whole process can be daunting for someone new to the language.

With the introduction of the Codable protocol in Swift 4, this process became much more streamlined. It’s like having an expert translator by your side who understands both languages fluently.

This translator can quickly and accurately convert entire sentences (or complex data structures) without you having to intervene at every step. It simplifies the task, reduces errors, and is much more beginner-friendly.

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!

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!

To better understand the process, think of the JSON data as a puzzle. Each piece of the puzzle represents a piece of data. The Codable protocol acts as a guide, helping you fit each piece in its correct place to form the complete picture, which in this case is the User object.

The beauty of Codable lies in its simplicity. Instead of manually mapping each piece of data, Codable does the heavy lifting for you. It understands the structure of your Swift object and the structure of the JSON data, and it maps them together seamlessly. This not only saves time but also reduces the chances of errors.

For those new to Swift, this might seem like magic. But in reality, it’s the result of Swift’s powerful type system and the flexibility of the Codable protocol. By conforming to Codable, you’re essentially telling Swift, “Hey, this object can be represented as JSON and vice versa.” And Swift takes care of the rest.

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.

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.

To better understand this, think of the JSON string as a coded message. In its current form, it’s not readily usable for our Swift application. We need to convert this message into a format that Swift can understand and work with. The Data object is this more usable format. It’s like decoding a secret message into plain English.

The process of converting the JSON string into a Data object is crucial because it prepares the data for further decoding into our desired Swift object. It’s a bit like prepping ingredients before cooking a dish. The ingredients, in their raw form, aren’t ready to be eaten. They need to be cleaned, cut, and prepped before they can be used in cooking.

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.

Conclusion

In the evolving landscape of Swift programming, understanding and effectively utilizing the Codable protocol is paramount for seamless data handling, especially when working with JSON. As developers, embracing these tools not only streamlines our coding process but also enhances the robustness and efficiency of our applications.

Whether you’re a seasoned developer or just starting out, the power of Codable in Swift is undeniable, offering a simplified approach to data encoding and decoding. Dive in, experiment, and harness the capabilities of Codable to elevate your app development journey.


Abhinav Girdhar

Founder and CEO of Appy Pie

App Builder

Most Popular Posts