Are you tired of writing boilerplate code just to convert JSON data to Swift objects? Or perhaps you’ve been wrestling with a complex API response that just doesn’t seem to fit into your neat Swift data structures? Well, it’s time to put all that behind you. Say hello to Codable, a protocol in Swift 4 and later that makes it a breeze to convert data between JSON format and your own Swift data types.
We’ll be working with real-world examples to see Codable in action. Our first example is a SwiftUI app where users can add URLs to a reading list. It also shows how to save a file to disc, which you can learn about with File Manager in Swift.
The second example is a fun one: we’ll fetch data from TheCatAPI and display information about various cat breeds. By the end of this post, you’ll have a solid understanding of Codable and will be able to simplify data conversion in your own apps.
⬇️ You can find the project files for the Reading List app here and the cat app here.
Understanding JSON
JSON is common data format, that is a text based data format. JSON stands for JavaScript Object Notation. It’s a data format that’s easy for humans to read and write, and easy for machines to parse and generate.
Think of JSON as a way of organizing data into key-value pairs, much like a dictionary. It uses simple syntax, with data structured through the use of curly braces {} to define objects, square brackets [] for arrays, and commas to separate data elements. Let’s look at a simple example:
{
"name": "John Doe",
"age": 30,
"isStudent": false,
"courses": ["Math", "Science", "History"]
}
In this JSON, you have an object (the stuff inside the curly braces). This object has four keys: “name”, “age”, “isStudent”, and “courses”. The values for these keys are a string, a number, a boolean, and an array, respectively.
Now, JSON is the go-to when it comes to APIs and web services. Why? Because it’s straightforward, lightweight, and can be easily sent over the internet. Let’s say you’re fetching data from a weather API. The server might send you JSON data that looks like this:
{
"city": "San Francisco",
"temperature": 22,
"weather": "sunny"
}
This JSON tells you that it’s a sunny 22 degrees in San Francisco. Neat, huh?
But it’s not just about online data. JSON can also be used to save user data locally on a device. Let’s imagine you’re building a reading list app. When a user adds a new book to their list, you could save that data in a JSON file like this:
{
"books": [
{
"title": "1984",
"author": "George Orwell",
"isRead": false
},
{
"title": "To Kill a Mockingbird",
"author": "Harper Lee",
"isRead": true
}
]
}
What is Codable in Swift?
Codable is a type alias in Swift that stands for two things: Decodable and Encodable protocols. If a type is Codable, that means it can convert itself into data (that’s the Encodable part), and it can also initialize itself from data (that’s the Decodable part). Super handy, right?
So why do we need the Codable protocols? Well, remember the JSON we talked about earlier? It’s great for sending data over the internet, but it’s not so great when we want to use that data in our Swift code. We need a way to convert that JSON into Swift objects, and that’s where Codable comes in.
Here’s the deal: when you fetch JSON data from an API, that data comes back as, well, data. It’s not a Swift object that you can use right away in your code. You have to decode that data into a Swift object. And if you want to send data to a server, you have to do the reverse: encode your Swift object into data. Codable makes this process a lot easier by doing most of the heavy lifting for you.
Making a Swift-Type Codable
As an example, I am going to use a reading list app, that lets the user enter URLs for web pages he/she wants to read later. The data needs to be saved on the device so that it is still there the next time the app is launched. You can see the app interface in the following example screenshots:
The first step is to define your Swift type. This could be a class, a struct, or even an enum. For now, we’ll stick with structs because they’re simple and lightweight.
Next, you’ll need to decide what properties your type should have. Remember, each property will map to a key in your JSON.
Once you’ve got your data type and its properties, you just need to add : Codable after your type’s name. This tells Swift that your type should conform to the Codable protocol.
Now let’s see this in action. Consider this struct, which we’ll use to store reading data in a SwiftUI app:
struct ReadingData: Codable, Equatable, Identifiable {
let url: URL?
let title: String
let creationDate: Date
var hasFinishedReading: Bool
var id: Date { return creationDate }
}
This ReadingData struct has a few properties: a URL (which is optional, hence the question mark), a title, a creation date, a boolean indicating whether the user has finished reading, and an id.
But notice the stuff after the colon: Codable, Equatable, Identifiable. Here’s what each one means:
- Codable: As you know, this means the struct can be encoded into data and decoded from data.
- Equatable: This means you can compare two instances of the struct for equality.
- Identifiable: This is a requirement for certain SwiftUI features. It means each instance of the struct has a unique identifier. In this case, we’re using the creation date as the id.
So how might you use this ReadingData struct in an app? Let’s say your app lets users save web pages to a reading list. When a user adds an item, you could create a new ReadingData instance with the web page URL and title, the current date, and hasFinishedReading set to false. You could then encode this ReadingData instance into data and save it locally using FileManager. Read about Managing Swift files.
Important: As long as all types that are used inside your custom struct conform to Codable (like String, Int, and Date types), your custom types can easily conform to Codable protocol. If you use complex json data with e.g. nested custom types inside, add Codable to all of them.
Encoding and Decoding JSON Objects
Let’s roll up our sleeves and dive into encoding and decoding with Codable. Remember our ReadingData struct from the previous section? Let’s use that for our examples.
Encoding a Swift Object to JSON
When you have a Swift object and you want to convert it to JSON, that’s called encoding. Here’s how you do it:
First, you’ll need an instance of the object you want to encode. Let’s create a ReadingData instance:
let article = ReadingData(url: URL(string: "https://example.com"),
title: "Interesting Article",
creationDate: Date(),
hasFinishedReading: false)
Now, to convert this ReadingData instance into JSON, you’ll use a JSONEncoder. Here’s how:
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601 // Use ISO 8601 date format
do {
let jsonData = try encoder.encode(article)
// jsonData now contains the JSON data for the article
if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString) // Prints the JSON string
}
} catch {
print("Error encoding article: \(error)")
}
If the json encoding above is successful, jsonData will contain the JSON data for the article. You can convert this data into a string to see the JSON.
Decoding JSON to a Swift Object
Now, what if you have some JSON and you want to convert it to a Swift object? That’s called decoding, and it’s just as easy.
If we continue the above example, the JSON data would look like this:
{
"title": "Interesting Article",
"url": "https://example.com",
"creationDate": "2023-05-19T14:36:00Z",
"hasFinishedReading": false
}
To convert this JSON into a ReadingData instance, you’ll use a JSONDecoder. Here’s how:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601 // Use ISO 8601 date format
do {
let article = try decoder.decode(ReadingData.self, from: jsonData)
// article is now a ReadingData instance
print(article.title) // Prints "Interesting Article"
} catch {
print("Error decoding JSON: \(error)")
}
If the decoding is successful, article will be a ReadingData instance that you can use in your code.
Notice the do-catch blocks in both examples? Encoding and decoding can fail if the JSON doesn’t match your Swift type, so you need to handle any errors that might occur.
And there you have it! You’ve just encoded a Swift object to JSON and decoded JSON to a Swift object. But what about nested objects and arrays? That’s up next. Stay tuned!
Error Handling for JSON Parsing
Dealing with JSON means dealing with uncertainty. You can’t always control the data you’re working with, especially when it’s coming from an API. That’s why error handling is a crucial part of working with Codable. Let’s talk about the kinds of errors that can crop up and how you can handle them.
Types of Errors
There are several types of errors that can occur when you’re encoding and decoding with Codable:
- DecodingError.typeMismatch: This occurs when the type you’re trying to decode doesn’t match the actual type in the JSON. For example, if you’re expecting a string but the JSON has an integer, you’ll get a typeMismatch error.
- DecodingError.valueNotFound: This happens when a required value is missing from the JSON. If your Swift type expects a certain key, but that key is not present in the JSON, you’ll get a valueNotFound error.
- DecodingError.keyNotFound: This is similar to valueNotFound, but it occurs when a key is missing from the JSON.
- EncodingError.invalidValue: This error occurs when you’re trying to encode a Swift object, but one of the values can’t be encoded. For instance, if you’re trying to encode a URL, but the URL is not valid, you’ll get an invalidValue error.
How to handle errors with a do-catch block
How do you handle these errors? Well, when you’re encoding or decoding with Codable, you’ll typically use a do-catchblock. This allows you to catch any errors that occur and handle them appropriately.
For instance, when you’re decoding, you could do something like this:
let decoder = JSONDecoder()
do {
let breed = try decoder.decode(Breed.self, from: jsonData)
// breed is now a Breed instance
} catch let DecodingError.typeMismatch(type, context) {
print("Type mismatch for \(type): \(context.debugDescription)")
} catch let DecodingError.valueNotFound(type, context) {
print("Value not found for \(type): \(context.debugDescription)")
} catch let DecodingError.keyNotFound(key, context) {
print("Key not found: \(key.stringValue) - \(context.debugDescription)")
} catch let error {
print("Error decoding JSON: \(error)")
}
In this example, each catch block handles a different type of error. If a typeMismatch error occurs, you’ll print a message with the mismatched type and a description of the error. If a valueNotFound or keyNotFound error occurs, you’ll print a similar message with the missing value or key. If any other error occurs, you’ll simply print a message with the error.
Error handling might not be the most glamorous part of working with Codable, but it’s one of the most important. By handling errors properly, you can ensure that your app runs smoothly, even when the data you’re working with is less than perfect.
Working with an API and Advanced JSON Format
Alright, you’ve got the basics down. Now let’s dive into some of the more advanced techniques you can use with Codable. For this, I’ll use another real-world example project: an app that fetches data about cat breeds from the TheCatAPI.
The following example is a JSON response from the server:
[{"weight":{"imperial":"7 - 10","metric":"3 - 5"},"id":"abys","name":"Abyssinian","cfa_url":"http://cfa.org/Breeds/BreedsAB/Abyssinian.aspx","vetstreet_url":"http://www.vetstreet.com/cats/abyssinian","vcahospitals_url":"https://vcahospitals.com/know-your-pet/cat-breeds/abyssinian","temperament":"Active, Energetic, Independent, Intelligent, Gentle","origin":"Egypt","country_codes":"EG","country_code":"EG","description":"The Abyssinian is easy to care for, and a joy to have in your home. They’re affectionate cats and love both people and other animals.","life_span":"14 - 15","indoor":0,"lap":1,"alt_names":"","adaptability":5,"affection_level":5,"child_friendly":3,"dog_friendly":4,"energy_level":5,"grooming":1,"health_issues":2,"intelligence":5,"shedding_level":2,"social_needs":5,"stranger_friendly":5,"vocalisation":1,"experimental":0,"hairless":0,"natural":1,"rare":0,"rex":0,"suppressed_tail":0,"short_legs":0,"wikipedia_url":"https://en.wikipedia.org/wiki/Abyssinian_(cat)","hypoallergenic":0,"reference_image_id":"0XYvRd7oD","image":{"id":"0XYvRd7oD","width":1204,"height":1445,"url":"https://cdn2.thecatapi.com/images/0XYvRd7oD.jpg"}}]
It is just a long text based data format. To make this more readable, you can use pretty print JSON in Swift like so:
if let data = try? JSONSerialization.data(withJSONObject: jsonFileData, options: .prettyPrinted) {
if let prettyPrinted = String(data: data, encoding: .utf8) {
print(prettyPrinted)
}
}
This will give you a more readable output:
[
{
"weight": {
"imperial": "7 - 10",
"metric": "3 - 5"
},
"id": "abys",
"name": "Abyssinian",
"cfa_url": "http://cfa.org/Breeds/BreedsAB/Abyssinian.aspx",
"vetstreet_url": "http://www.vetstreet.com/cats/abyssinian",
"vcahospitals_url": "https://vcahospitals.com/know-your-pet/cat-breeds/abyssinian",
"temperament": "Active, Energetic, Independent, Intelligent, Gentle",
"origin": "Egypt",
"country_codes": "EG",
"country_code": "EG",
"description": "The Abyssinian is easy to care for, and a joy to have in your home. They’re affectionate cats and love both people and other animals.",
"life_span": "14 - 15",
"indoor": 0,
"lap": 1,
"alt_names": "",
"adaptability": 5,
"affection_level": 5,
"child_friendly": 3,
"dog_friendly": 4,
"energy_level": 5,
"grooming": 1,
"health_issues": 2,
"intelligence": 5,
"shedding_level": 2,
"social_needs": 5,
"stranger_friendly": 5,
"vocalisation": 1,
"experimental": 0,
"hairless": 0,
"natural": 1,
"rare": 0,
"rex": 0,
"suppressed_tail": 0,
"short_legs": 0,
"wikipedia_url": "https://en.wikipedia.org/wiki/Abyssinian_(cat)",
"hypoallergenic": 0,
"reference_image_id": "0XYvRd7oD",
"image": {
"id": "0XYvRd7oD",
"width": 1204,
"height": 1445,
"url": "https://cdn2.thecatapi.com/images/0XYvRd7oD.jpg"
}
}
]
This JSON object represents a single cat breed, the Abyssinian, along with various details about the breed. As you can see, the JSON structure is a nested combination of arrays and dictionaries (objects), which is a common structure for JSON data. The Codable protocol in Swift can handle such nested structures with ease, which is one of its major advantages.
Here is how we could transfer this data into a Swift data type:
struct Breed: Codable, CustomStringConvertible, Identifiable {
let id: String
let name: String
let temperament: String
let breedExplaination: String
let energyLevel: Int
let isHairless: Bool
let image: BreedImage?
var description: String {
return "breed with name: \(name) and id \(id), energy level: \(energyLevel) isHairless: \(isHairless ? "YES" : "NO")"
}
enum CodingKeys: String, CodingKey {
case id
case name
case temperament
case breedExplaination = "description"
case energyLevel = "energy_level"
case isHairless = "hairless"
case image
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
name = try values.decode(String.self, forKey: .name)
temperament = try values.decode(String.self, forKey: .temperament)
breedExplaination = try values.decode(String.self, forKey: .breedExplaination)
energyLevel = try values.decode(Int.self, forKey: .energyLevel)
let hairless = try values.decode(Int.self, forKey: .isHairless)
isHairless = hairless == 1
image = try values.decodeIfPresent(BreedImage.self, forKey: .image)
}
}
I am not using all the properties that are included in the JSON file. I only pick the ones that I want to use for my app.
This Breed custom type is a bit more complex than our ReadingData struct from the above example. It has more properties, and it uses a few techniques that we haven’t seen yet.
Coding Keys
The Coding Keys enum is a big one. This is how you handle JSON data that doesn’t map directly to a Swift object. Each case in the enum corresponds to a property of the struct, and the raw value of each case is the coding key that should be used in the JSON.
During the decoding process, the decoder looks for all coding keys in the JSON. For example, the JSON data object has a field with the name “description”. The property names in Swift should not include this name. I have to use a different property name, which is breedExplaination. In order for the decoder to know how to map from the json keys in the JSON data to my Swift struct, it uses the coding keys’ raw values. In his case, it uses the case breedExplaination to look for the property name in the JSON and adds the value the Swift structs property breedExplaination.
Custom Initializer
The custom initializer is where the magic happens. This is where you decode the JSON data and assign it to the properties of the struct.
The init(from:) method is required by the Decodable protocol. It takes a Decoder as an argument, and it throws an error if the decoding fails.
In this initializer, you first create a container using the CodingKeys enum. Then, for each property of the struct, you decode a value from the container. If the decoding is successful, you assign the decoded value to the property.
How to transform different types from JSON to Swift
One interesting thing to note here is how the Breed struct handles the isHairless property. The API represents “hairless” as an integer (1 for true, 0 for false), but in our Swift struct, we want to use a boolean. The initializer handles this by decoding the value as an integer, then converting it to a boolean.
Optional fields: How to deal with properties that are only sometimes passed?
Another technique is dealing with optional fields. The image property is optional because not all breeds may have images. It attempts to decode a value, but if the value is missing, it assigns nil to the property.
image = try values.decodeIfPresent(BreedImage.self, forKey: .image)
These are just a few of the advanced techniques you can use with Codable. Remember, your goal is to bridge the gap between the JSON data and your Swift objects. Sometimes that’s straightforward, but other times you’ll need to be a bit more creative.
Conclusion
So far, we’ve explored Swift’s `Codable` protocol and how it simplifies the process of converting JSON data into Swift objects and vice versa. We started with understanding what JSON is and its importance in data transfer, especially in APIs. Then, we delved into the `Codable` protocol in Swift and its constituents: `Encodable` and `Decodable`.
We learned how to make our Swift types conform to `Codable`, using our reading list app as an example. We explored how to encode and decode JSON data, handling arrays and nested objects along the way.
We didn’t stop there, though. We saw how to handle errors that might occur during encoding and decoding, preparing us for real-world scenarios where the data might not always be perfect. Finally, we ventured into some advanced techniques with `Codable`. We looked at another example app that fetches cat breed data from an API, handling optional fields and customizing our types to match the structure of the JSON data.
By now, you should feel more comfortable working with JSON data in Swift. Remember, practice makes perfect. Don’t hesitate to experiment with different JSON structures and Swift types. Happy coding!
How to go from here:
- learn about File Manager in Swift: Reading, Writing, and Deleting Files and Directories
- watch a tutorial about Working with the web to get to know APIs and http requests
- see how to use a REST Api with JSON in a SwiftUI app in this Youtube tutorial