An introduction to JSON parsing in Swift
Published on: April 5, 2021Virtually 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.
If you prefer to consume the contents of this post as a video, you can watch the video below.
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.