JSON Decoding in Swift with Codable: A Practical Guide

Encoding and decoding data is a fundamental part of iOS apps.

That is especially true for the JSON data we get from REST APIs.

In the past, decoding JSON in iOS apps required a lot of boilerplate code and sometimes fancy techniques.

But thanks to the Codable protocols introduced of Swift 4, today we have a native and idiomatic way to encode and decode data.

The Codable protocols allow for simple JSON decoding that can sometimes take only a couple of lines of code. But they also allow for more sophisticated techniques when you have special needs.

We will explore all that in this article.

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Contents


TOC 2

Section 1

Mapping JSON data to Swift types and automatic decoding


TOC 1

Section 2

Custom decoding with coding keys and nested JSON objects


TOC 3

Section 3

Flattening JSON data and decoding arrays with decoding containers


Section 4

Decoding JSON data in a real iOS app

Section 1:

Mapping JSON data to Swift types and automatic decoding

Mapping JSON data to Swift types and automatic decoding

The first thing you need to take care of when decoding JSON data is how to represent it in your app using Swift types. After mapping data to Swift types, you can easily decode data in a few lines of code.

The relationship between JSON data and the model types in your app

Before we get into the nitty-gritty of decoding, we need to clarify the relationship between code and data.

Any iOS app needs to deal with data. So we need a way to represent it and its business logic in code.

In iOS, we develop apps following the MVC pattern. In MVC, we represented data in the model layer, where we use structures and enumerations to represent data entities and their business logic.

That is the app’s internal representation of its data.

But that’s not enough.

Software often stores data permanently and communicates with other software. For that, we need an external representation of our data.

To allow communication, we need a standard format understood by applications written in different languages.

In today’s internet, the two most popular formats to represent data are JSON and XML.

JSON is the most popular of the two because it has a simpler structure that can be efficiently encoded and decoded. So, most REST APIs return data in JSON format.

Where you should put the code to encode and decode JSON data

Since an app has to deal with an internal and an external representation of its data, we need some code to convert from one to the other, and vice-versa.

But where do we put that code?

I have been for a long time an advocate of putting transformation code into model types. This allows you to keep view controllers lean and avoid massive view controllers.

So, when the Codable protocols were introduced, I was quite pleased to see that they force you to put transformation code into model types.

When you want to decode or encode some JSON data, the corresponding model types in your project need to conform to the Decodable and Encodable protocols respectively.

If you need to do both, you can use the Codable typealias instead, which is just a combination of the other two protocols.

Sometimes, that’s all you need to do.

All the encoding and decoding code gets generated automatically for you by the Swift compiler.

And while the Codable protocols are often used for JSON decoding, they also work for other formats, like, for example, property lists.

The three-step process to decode JSON data in Swift

In any app, you have to go through three steps to decode the JSON data you get from a REST API.

  1. Perform a network request to fetch the data.
  2. Feed the data you receive to a JSONDecoder instance.
  3. Map the JSON data to your model types by making them conform to the Decodable protocol.

You don’t have to write your code in this order necessarily. It is quite common to start from the last step and proceed backward, which is what we will do in this article.

To see a practical example, we need some JSON data and, ideally, a remote API that provides it. Luckily, I found this handy list of public APIs, where we have plenty to choose from.

For this article, I will use the SpaceX API. Since I published this article, a new version (v4) has been introduced. This article uses v3, but it makes no difference to the concepts I’ll be explaining.

We will build a small app with two screens. One that shows a list of SpaceX rocket launches, and one that shows the detail of a selected launch.

Mockup

While small, this app will cover all the common cases you find when decoding JSON data.

You can find the code for the whole app on GitHub.

Creating a mapping between a model type and the corresponding JSON data

As a start, let’s grab some JSON data representing a launch from the API.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_unix": 1532238600,
	"launch_success": true
}

The full JSON data for a launch is longer than this, but we can safely ignore any property we don’t need.

The first thing we need is a model type (a structure) that represents a launch.

struct Launch: Decodable {
	let flightNumber: Int
	let missionName: String
	let launchDateUnix: Date
	let launchSuccess: Bool
}

The most straightforward way to create a mapping between a model type and the corresponding JSON data is to:

  • make the type conform to Decodable, and
  • name the type’s stored properties following the names in the JSON data.

For this to work, every property in our Launch structure needs to have a type that also conforms to Decodable. Luckily, all common Swift types like Bool, Int, Float, and even arrays conform to both the Codable protocol.

Copying the names JSON fields exactly though would create some atypical Swift code.

JSON uses snake case, which is a common practice in web apps. In Swift, we use camel case instead. But luckily, we can match the two.

Now that we have a mapping between our type and the JSON data, we can feed the latter to an instance of JSONDecoder and get back a Launch value.

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
let launch = try decoder.decode(Launch.self, from: jsonData)

The convertFromSnakeCase option for the keyDecodingStrategy is what allows us to use camel case instead of snake case for the properties in our structure.

Dates in JSON can be expressed in different formats, so we have to tell the JSONDecoder which one to expect by setting its dateDecodingStrategy.

In this case, the launch_date_unix field in the JSON data is expressed in Unix time, which is the count of seconds that passed since January 1st, 1970. We will talk more about other date formats later in the article.

That’s it.

We only needed a couple of lines of code to decode our JSON data.

How to get some JSON data from a remote API to write your decoding code

We don’t yet have any code to perform network requests and fetch some JSON data from the remote API, so how do we know if our code is correct?

Granted, our code is still so simple that it’s hard to get it wrong.

But it’s going to grow in complexity, and we don’t want to wait until we have a full app to discover that decoding is broken.

First of all, we need some real JSON data.

If the API you use does not require any form of authentication, you can open the URL of a resource directly in your browser. JSON data is just text, so you can copy and paste it.

Usually, all the white space is stripped to save bandwidth, so raw JSON data is not very readable. Here you have a couple of options.

  • You can first put it inside an online JSON formatter. These formatters usually offer validation too, in case you want to check if some data you generate is valid.
  • You can then copy the output and save it in a file with .json extension, for later use. Xcode allows you also to fold JSON data.

using xcode code folding to fold JSON data

A better alternative is Postman, which is a free app that allows you to make network requests. 

Postman allows you not only to get data but also to send it using other HTTP methods like POST. Plus:

  • it lets you specify authentication headers
  • it formats the data in a response,
  • it allows you to save the requests you make,
  • and more.

using postman to make network requests and to format JSON data

Testing that your decoding code actually works

The most straightforward way to test your decoding code is to use a Swift Playground.

A couple of caveats to keep in mind:

  • To write the JSON data in a string that spans multiple lines, use a multiline string literal delimited by triple quotes.
  • A JSONDecoder works with binary data. To decode a JSON string, you have first to convert it to a Data value using the data(using:) method. The format you have to use is .utf8
import Foundation

let json = """
{
    "flight_number": 65,
    "mission_name": "Telstar 19V",
    "launch_date_unix": 1532238600,
    "launch_success": true
}
"""

struct Launch: Decodable {
	let flightNumber: Int
	let missionName: String
	let launchDateUnix: Date
	let launchSuccess: Bool
}


let jsonData = json.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
let launch = try decoder.decode(Launch.self, from: jsonData)

If you paste the above code in a Swift playground, you can see the result of the decoding directly below your code.

using an xcode playground to test JSON decoding in Swift

The second way to test your decoding code is to write a unit test for it.

First of all, save the data in a .json file in the testing bundle of your project.

saving JSON data in a file to test Swift decoding

Then, write a unit test where you

  1. read the file;
  2. decode its data;
  3. check each property of the resulting Launch value.
import XCTest
@testable import Launches

class LaunchesTests: XCTestCase {
	func testLaunchDecoding() {
		let bundle = Bundle(for: type(of: self))
		guard let url = bundle.url(forResource: "Launch", withExtension: "json"),
			let data = try? Data(contentsOf: url) else {
				return
		}
		
		let decoder = JSONDecoder()
		decoder.keyDecodingStrategy = .convertFromSnakeCase
		decoder.dateDecodingStrategy = .secondsSince1970
		guard let launch = try? decoder.decode(Launch.self, from: data) else {
			return
		}
		
		XCTAssertEqual(launch.flightNumber, 65)
		XCTAssertEqual(launch.missionName, "Telstar 19V")
		XCTAssertEqual(launch.launchDateUnix, Date(timeIntervalSince1970: 1532238600))
		XCTAssertEqual(launch.launchSuccess, true)
	}
}

This is, of course, longer, but it has all the benefits of unit testing. The code you write in a playground is useful only for a quick test, while you can re-run a unit test to keep your code from breaking.

Section 2:

Custom decoding with coding keys and nested JSON objects

Automatic decoding is enough only for the most straightforward cases but does not respect Swift stylistic conventions. You can use coding keys to gain more flexibility in your model types.

Customizing the names of your types’ stored properties with coding key

It is great to be able to decode JSON data in so few lines of code.

But while a JSONDecoder can convert from snake case to camel case, most of the time the property names we get when we mirror the JSON data do not follow the Swift’s API Design Guidelines.

For example, in our Launch structure the flightNumber and missionName properties are ok, but:

  • launchDateUnix is long and misleading. In the JSON, the date is in Unix format, but in our code, it’s a Date.
  • launchSuccess does not follow the Swift convention of naming booleans to read like assertions.

It would be great if we could rename these properties.

And, thanks to coding keys, we can.

The coding keys of a Codable type are represented by a nested enumeration that:

  • is named CodingKeys;
  • conforms to the CodingKey protocol;
  • has a String raw value.

Notice the plural in the enumeration name and the singular in the protocol name.

The compiler requires the enumeration name to be exact. If you misspell it, you can waste a lot of time understanding why your decoding does not work.

The cases of the enumeration need to be named after the properties of your structure. Their raw values instead need to correspond to the names in the JSON data.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
}

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_unix"
		case succeeded = "launch_success"
	}
}

I usually use extensions for protocol conformance, to keep it separate from the type itself. This allows me to move it to another file if I want.

But you can put the CodingKeys enumeration in the Launch type itself if you prefer.

How to decode dates in ISO 8601 format (or other custom formats)

In software, dates come in many different formats. That also happens in JSON data.

Luckily, Apple already thought about it and provided different date decoding strategies for the most common date formats.

Above we used the .secondsSince1970 strategy for Unix dates. Another common date format you will find in JSON data is ISO 8601.

The SpaceX API provides the launch date in many formats, including the ISO 8601.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_utc": "2018-07-22T05:50:00.000Z",
	"launch_success": true,
}

In theory, decoding ISO 8601 dates should be simple. In practice, it can create some problems.

First of all, we need to change the mapping in the CodingKeys enumeration.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
}

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
	}
}

Then, we need to change the date decoding strategy of the JSONDecoder to .iso8601.

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let launch = try decoder.decode(Launch.self, from: data)

Unfortunately, that does not work.

It seems that the JSONDecoder class does not like all the possible ISO 8601 date formats, but only a subset.

Luckily though, we can fix this problem by providing a custom date formatter.

extension DateFormatter {
	static let fullISO8601: DateFormatter = {
		let formatter = DateFormatter()
		formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
		formatter.calendar = Calendar(identifier: .iso8601)
		formatter.timeZone = TimeZone(secondsFromGMT: 0)
		formatter.locale = Locale(identifier: "en_US_POSIX")
		return formatter
	}()
}

We can then easily pass our custom formatter to the JSONDecoder using the .formatted(_:) date decoding strategy.

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
let launch = try decoder.decode(Launch.self, from: jsonData)

Decoding nested JSON objects

JSON allows the encoding of structured data with nested objects.

It is rare to find an API that returns only plain JSON objects. Most of the time, a JSON object contains other ones.

This also happens in the SpaceX API we are using.

The JSON data for a launch contains many other objects. A particular one in which we are interested is the timeline of a launch.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_utc": "2018-07-22T05:50:00.000Z",
	"launch_success": true,
	"timeline": {
		"go_for_prop_loading": -2280,
		"liftoff": 0,
		"meco": 150,
		"payload_deploy": 1960
	}
}

The simplest way to deal with nested JSON objects in Swift is to create nested types that mirror the nesting in the data.

First of all, we need a decodable structure representing a timeline.

struct Timeline {
	let propellerLoading: Int?
	let liftoff: Int?
	let mainEngineCutoff: Int?
	let payloadDeploy: Int?
}

extension Timeline: Decodable {
	enum CodingKeys: String, CodingKey {
		case propellerLoading = "go_for_prop_loading"
		case liftoff
		case mainEngineCutoff = "meco"
		case payloadDeploy = "payload_deploy"
	}
}

This time, I made the properties of Timeline optional, because any of them might be missing in the JSON data.

If a property is not optional but is missing from the data, the decoder throws an error. We will talk about errors later.

Once you have a structure for a nested object, all you need to do is add a property to the type that contains it.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
	let timeline: Timeline?
}

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
	}
}

The timeline property is also optional because failed launches don’t have a timeline, so the timeline field might be missing altogether.

Section 3:

Flattening JSON data and decoding arrays with decoding containers

Flattening JSON data and decoding arrays with decoding containers

Mirroring the structure of JSON data often produces types used only for decoding. If you want to break free from the constraints of the data you decode, you need to write custom decoding code using decoding containers.

Your model types do not have to mirror the structure of JSON data

Decoding nested JSON object using nested Swift structures is straightforward, but sometimes it might not make sense in our apps.

No rule forces you to mirror the structure of the JSON data you receive from a remote API.

In fact, most of the times, you shouldn’t.

Your model types should reflect the business logic of your app, even if that diverges from the data you read from an external source.

For example, in our little app we need some more information about a launch:

  • the name of the rocket used;
  • the name of the launch site; and
  • the URL for the patch.

In the JSON data, these three pieces of information are inside three different nested objects.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_utc": "2018-07-22T05:50:00.000Z",
	"launch_success": true,
	"rocket": {
		"rocket_id": "falcon9"
	},
	"launch_site": {
		"site_name_long": "Cape Canaveral Air Force Station Space Launch Complex 40"
	},
	"links": {
		"mission_patch": "https://images2.imgbox.com/c5/53/5jklZkPz_o.png"
	}
	"timeline": {
		"go_for_prop_loading": -2280,
		"liftoff": 0,
		"meco": 150,
		"payload_deploy": 1960
	}
}

We can decode these too by creating nested Swift structures, but they would all have one property.

These new types would not make any sense. They would exist only to decode the JSON data, which is not a reasonable justification.

It’s better to add these three properties to the Launch structure and give them simple types.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
	let timeline: Timeline?
	let rocket: String
	let site: String
	let patchURL: URL
}

Notice that we only have a URL for the patch.

Remember that JSON data is a string and cannot carry binary data like images, audio or video files.

While sometimes you can convert those to a string using base 64 encoding, it is more common for JSON data to carry URLs to other resources that you have to download separately.

Providing coding keys for nested JSON objects

Since our Launch structure does not mirror the JSON data anymore, we can’t use automatic decoding.

The compiler can match model types and data only when they correspond. When they deviate, you need to provide more code.

The first thing we need is some more coding keys for the nested objects we want to read.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
}

Notice that the new cases for the CodingKeys enumeration follow only the names in the JSON data. The Launch structure does not have links and launchSite properties.

Since we are going to provide our decoding code, that is not a requirement anymore.

Actually, now we could also change the name of the CodingKeys enumeration. It must be named that way only when we use automatic decoding.

I also added three new coding key enumerations, for the three nested objects we want to read.

I nested those inside CodingKeys, but that’s not required. All you need is an enumeration for JSON object you want to decode. You can declare them outside of the CodingKeys enumeration.

I nest them to keep a correspondence between my code and the JSON data. This makes it easier to read my code later when I will have forgotten how it works.

Decoding values and flattening nested objects using keyed decoding containers

Now that we have coding keys enumeration for all the JSON objects we want to decode, we need to provide our custom decoding code.

You place such code in the required init(from:) initializer of the Decodable protocol.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {

	}
}

Even if this initializer is required, we didn’t have to implement it until now. That’s because the compiler synthesizes it for you if you don’t provide one.

Since we can’t use automatic decoding, the synthesized initializer does not fit our needs anymore. Unfortunately, this also means that we now have to decode all the properties ourselves. 

There is no middle ground. You either use automatic decoding and let the compiler synthesize the whole initializer, or you decode every single property by yourself.

To do that, we need to use a keyed decoding container. Containers allow us to read any value we want directly from the JSON data.

We get the container for a launch object from the decoder parameter we get in the initializer.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
	}
}

When you call the container(keyedBy:) method, you need to specify which coding keys enumeration to use. That’s why now we can name our enumerations as we please.

When you read the value of a property, you need to specify what type it has and which key to use to retrieve it.

We now need to read the properties contained inside the three nested JSON objects.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
	}
}

As you can see, the process is the same.

  • First, we get a container for a nested object from the current container, keyed with the appropriate enumeration.
  • Then, we retrieve the values we want from the nested container.

Decoding array fields using nested arrays of concrete Swift types

JSON objects do not only contain other objects but can also include arrays. This is a complication you often meet when dealing with JSON data from remote APIs.

And indeed, that happens also in the data for a SpaceX launch.

A rocket can deploy one or more payloads at the same time, so these come as an array in the JSON data.

{
	"flight_number": 65,
	"mission_name": "Telstar 19V",
	"launch_date_utc": "2018-07-22T05:50:00.000Z",
	"launch_success": true,
	"rocket": {
		"rocket_id": "falcon9",
		"second_stage": {
			"payloads": [
				{
					"payload_id": "Telstar 19V",
				}
			]
		}
	},
	"launch_site": {
		"site_name_long": "Cape Canaveral Air Force Station Space Launch Complex 40"
	},
	"links": {
		"mission_patch": "https://images2.imgbox.com/c5/53/5jklZkPz_o.png"
	}
	"timeline": {
		"go_for_prop_loading": -2280,
		"liftoff": 0,
		"meco": 150,
		"payload_deploy": 1960
	}
}

To complicate things further, the payloads are nested in the second_stage object, which is nested in the rocket object.

The nesting, by itself, is not a big problem. As before, we need to create some more coding keys to dig deeper into the nested structures.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
			case secondStage = "second_stage"

			enum SecondStageKeys: String, CodingKey {
				case payloads
			}
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
	}
}

Nothing new.

Now that we have a container for the second_stage object, we have to deal with its payloads field, which is our array.

There are two ways to decode arrays. The simplest one is to rely on concrete types, as we did for the timeline.

The process is the same. First, we create a new Payload structure that conforms to Decodable, with its coding keys for the fields we want to read.

struct Payload {
	let name: String
}

extension Payload: Decodable {
	enum CodingKeys: String, CodingKey {
		case name = "payload_id"
	}
}

We can now decode the array like we decode any other value, by passing [Payload].self as the type parameter to the decode(_:forKey:) method of the keyed decoding container.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
			case secondStage = "second_stage"

			enum SecondStageKeys: String, CodingKey {
				case payloads
			}
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
		let payloads = try secondStageContainer.decode([Payload].self, forKey: .payloads)
		self.payloads = !payloads.isEmpty
			? payloads.dropFirst().reduce("\(payloads[0].name)", { $0 + ", \($1.name)" })
			: ""
	}
}

The crucial line in the new code above is the first one, where we decode the array. The other three take the names of each payload and concatenate them into a single string for the payloads property of Launch.

I used the reduce(_:_:) function here because I like to use the functional programming approach for this kind of code. But you can use a common for loop instead if you prefer.

Avoiding extra types for array fields by using unkeyed decoding containers

While more straightforward, the method above still requires creating an extra structure which we only use for decoding.

And again, the Payload structure has only one property, so it does not make sense as a separate type.

As before, we would like to avoid creating useless types. For that, we need to use an unkeyed decoding container.

Unlike its keyed counterpart, an unkeyed container contains a sequence of values that are not identified by keys.

Or, more simply, an array.

From an unkeyed container, we can then extract keyed containers for each element in the array. These work the same way we saw above. So we don’t need to create any extra type

In our JSON data, the payloads field is going to be represented by an unkeyed container, from which we will extract a keyed container for every single payload.

As usual, we need a coding keys enumeration for these keyed containers, which is nothing else than the one we put in the Payload structure.

So we have to move that enumeration out of the Payload structure and then delete the latter.

extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
			case secondStage = "second_stage"

			enum SecondStageKeys: String, CodingKey {
				case payloads
				
				enum PayloadKeys: String, CodingKey {
					case payloadName = "payload_id"
				}
			}
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
		let payloads = try secondStageContainer.decode([Payload].self, forKey: .payloads)
		self.payloads = !payloads.isEmpty
			? payloads.dropFirst().reduce("\(payloads[0].name)", { $0 + ", \($1.name)" })
			: ""
	}
}

Again, I nested it inside SecondStageKeys, but you could declare it independently.

Cycling over the content of an unkeyed decoding container

We can now get the list of payloads for a launch by:

  • getting an unkeyed container for the payloads field that contains the array of payloads;
  • looping over its nested keyed containers and read the payload_id of each payload.
extension Launch: Decodable {
	enum CodingKeys: String, CodingKey {
		case timeline
		case links
		case rocket
		case flightNumber = "flight_number"
		case missionName = "mission_name"
		case date = "launch_date_utc"
		case succeeded = "launch_success"
		case launchSite = "launch_site"
		
		enum RocketKeys: String, CodingKey {
			case rocketName = "rocket_name"
			case secondStage = "second_stage"

			enum SecondStageKeys: String, CodingKey {
				case payloads
				
				enum PayloadKeys: String, CodingKey {
					case payloadName = "payload_id"
				}
			}
		}
		
		enum SiteKeys: String, CodingKey {
			case siteName = "site_name_long"
		}
		
		enum LinksKeys: String, CodingKey {
			case patchURL = "mission_patch"
		}
	}
	
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		flightNumber = try container.decode(Int.self, forKey: .flightNumber)
		missionName = try container.decode(String.self, forKey: .missionName)
		date = try container.decode(Date.self, forKey: .date)
		succeeded = try container.decode(Bool.self, forKey: .succeeded)
		timeline = try container.decodeIfPresent(Timeline.self, forKey: .timeline)
		
		let linksContainer = try container.nestedContainer(keyedBy: CodingKeys.LinksKeys.self, forKey: .links)
		patchURL = try linksContainer.decode(URL.self, forKey: .patchURL)
		
		let siteContainer = try container.nestedContainer(keyedBy: CodingKeys.SiteKeys.self, forKey: .launchSite)
		site = try siteContainer.decode(String.self, forKey: .siteName)
		
		let rocketContainer = try container.nestedContainer(keyedBy: CodingKeys.RocketKeys.self, forKey: .rocket)
		rocket = try rocketContainer.decode(String.self, forKey: .rocketName)
		let secondStageContainer = try rocketContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.self, forKey: .secondStage)
		
		var payloadsContainer = try secondStageContainer.nestedUnkeyedContainer(forKey: .payloads)
		var payloads = ""
		while !payloadsContainer.isAtEnd {
			let payloadContainer = try payloadsContainer.nestedContainer(keyedBy: CodingKeys.RocketKeys.SecondStageKeys.PayloadKeys.self)
			let payloadName = try payloadContainer.decode(String.self, forKey: .payloadName)
			payloads += payloads == "" ? payloadName : ", \(payloadName)"
		}
		self.payloads = payloads
	}
}

Notice that, while an unkeyed container represents an array, it does not behave like one. We cannot use a for loop to go over its content. Instead, we have to extract the keyed containers one by one, calling the nestedContainer(keyedBy:) method.

This is a mutating method with side effects. Not only it returns a keyed container, but it also makes the unkeyed container move to the next element.

That’s why I declared payloadsContainer to be a var variable and not a let constant.

Each keyed container we extract works like all other keyed containers we already saw, so we use the usual decode(_:forKey:) method to extract a payload’s name.

Finally, to end the loop, we need to check the isAtEnd property on the unkeyed container to know when we processed all the elements in the array.

Migrating old encoded data when your model types evolve

Before we go on and see how to decode data in an actual iOS app, I want to mention something that is not often discussed.

The Codable protocols work great to decode JSON data. Until they don’t.

One of the drawbacks of using Codable is that your model types are tightly coupled to your data. But data and model types change with time.

Data might change because remote APIs are sometimes updated. And if you are using a public API, that is out of your control.

Your model types might also change to adapt to your app’s domain business logic. Remote APIs only provide data but should not define your model types.

In both cases, you will end up with model types and data that don’t match anymore.

That’s not a problem per se. Fixing the mismatch is usually pretty straightforward.

But often, you will need to be able to decode both old and new encodings. That is especially true if you save data locally or use JSON encoding to create documents in a document-based app.

When that happens, you need to be able to migrate old data. That is a complex process that does not fit here, but you can read about that in my article on advanced swift techniques.

Section 4:

Decoding JSON data in a real iOS app

Decoding JSON data in a real iOS app

Decoding JSON data is only one of the parts of an iOS app. In a real-world project, you need to also fetch the data from a remote API, chain network requests, and populate your SwiftUI views.

Using static JSON files to create test data for development

While our app will fetch its data from the SpaceX API, it’s a good practice to use some local data to test what we build without connecting to the internet.

Luckily, we can already take advantage of the decoding code we wrote above.

We can grab some JSON data again from the API, copying it from the browser into a static .json file.

Since we need such data only for testing, we can place our new file in the Preview Content group of our Xcode project. Doing so excludes our test data from the final build we will submit to the App Store and our users.

launches

We also need some images. Again, to test our code offline, it’s better to add them to our project. We can put them in the Preview Assets catalog, which is part of the Preview Content group.

preview assets

To store the images, we need an additional property in the Launch type.

struct Launch {
	let flightNumber: Int
	let missionName: String
	let date: Date
	let succeeded: Bool
	let timeline: Timeline?
	let rocket: String
	let site: String
	let patchURL: URL
	let payloads: String
	var patch: UIImage?
}

I named each image after the id of its corresponding launch, so it will be easy to match them in our code.

And, finally, we can create our test data.

struct TestData {
	static var launches: [Launch] = {
		let url = Bundle.main.url(forResource: "Launches", withExtension: "json")!
		let data = try! Data(contentsOf: url)
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		var launches = try! decoder.decode([Launch].self, from: data)
		for (index, var launch) in launches.enumerated() {
			launch.patch = UIImage(named: "\(launch.id)")
			launches[index] = launch
		}
		return launches
	}()
}

This method opens the Launches.json file and decodes its content. The extra loop sets the corresponding patch image for each launch.

Since this is test data, we don’t have to worry about optionals and errors. Using the ! and try! operators can point us to decoding problems during development.

If you have more Codable types in your app, you can reuse your decoding code using Swift generics. Even though generics might be too advanced for you at the moment, you can copy and paste the code from that article in your TestData structure.

The view layer should not be concerned with the app’s global state or networking

We can use the test data we created to build some SwiftUI views for our user interface quickly. Building SwiftUI interfaces and structuring their data flow is behind the point of this article, so I will go over them quickly.

Nonetheless, there is an important lesson at the end of this lesson, so be sure to read it.

I designed both screens of our sample app with a common scrolling interface to keep things simple, where rows have an image, a title, and two subtitles. So, let’s start there:

struct Row: View {
	let image: UIImage?
	let title: String
	let subtitle1: String
	let subtitle2: String
	
	var body: some View {
		HStack(spacing: 16.0) {
			Image(uiImage: image ?? UIImage())
				.resizable()
				.frame(width: 62.0, height: 62.0)
			VStack(alignment: .leading, spacing: 2.0) {
				Text(title)
					.font(.headline)
				Group {
					Text(subtitle1)
					Text(subtitle2)
				}
				.font(.subheadline)
				.foregroundColor(.gray)
			}
		}
		.padding(.vertical, 16.0)
	}
}

Using our test data, we can preview our Row view.

struct LaunchInfoView_Previews: PreviewProvider {
	static let launch = TestData.launches[0]
	
	static var previews: some View {
		Row(image: launch.patch,
			title: launch.missionName,
			subtitle1: launch.date.formatted,
			subtitle2: launch.succeeded.formatted)
			.padding()
			.previewLayout(.sizeThatFits)
	}
}

preview

With our Row structure, we can create the list of launches for our app’s first screen.

</codestruct LaunchesView: View {
	let launches: [Launch]
	
	var body: some View {
		List {
			ForEach(launches) { launch in
				NavigationLink(destination: DetailView(launch: launch)) {
					Row(
						image: launch.patch,
						title: launch.missionName,
						subtitle1: launch.date.formatted,
						subtitle2: launch.succeeded.formatted)
				}
			}
		}
		.navigationBarTitle("Launches")
	}
}

struct LaunchesView_Previews: PreviewProvider {
	static var previews: some View {
		NavigationView {
			LaunchesView(launches: TestData.launches)
		}
	}
}

launches view

The screen for a single launch needs a header that displays the information of a launch.

struct Header: View {
	let launch: Launch
	
	var body: some View {
		VStack(spacing: 24.0) {
			Image(uiImage: launch.patch ?? UIImage())
				.resizable()
				.frame(width: 128.0, height: 128.0)
			VStack(spacing: 8.0) {
				Text(launch.missionName)
					.font(.largeTitle)
					.bold()
				Group {
					Text(launch.date.formatted)
					Text(launch.succeeded.formatted)
				}
				.foregroundColor(.gray)
				.font(.subheadline)
			}
		}
		.padding(.top, 32.0)
		.padding(.bottom, 40.0)
	}
}

struct LaunchDetailView_Previews: PreviewProvider {
	static var previews: some View {
		Header(launch: TestData.launches[0])
			.padding()
			.previewLayout(.sizeThatFits)
	}
}

header

With this extra view and the Row type, we can use a List again and create the whole Launch Details screen.

struct DetailView: View {
	let launch: Launch
	
	var payloadDeployment: String { launch.timeline?.payloadDeploy?.formatted ?? "" }
	var mainEngineCutoff: String { launch.timeline?.mainEngineCutoff?.formatted ?? "" }
	var liftoff: String { launch.timeline?.liftoff?.formatted ?? "" }
	var propellerLoading: String { launch.timeline?.propellerLoading?.formatted ?? "" }
	
	var body: some View {
		List {
			Header(launch: launch)
				.frame(maxWidth: .infinity)
			Row(image:  imageLiteral(resourceName: "Payload"),
				title: "Payload Deployment",
				subtitle1: "Time: " + payloadDeployment,
				subtitle2: "Payloads: \(launch.payloads)")
			Row(image:  imageLiteral(resourceName: "Cutoff"),
				title: "Main Engine Cutoff",
				subtitle1: "Time: " + mainEngineCutoff,
				subtitle2: "")
			Row(image:  imageLiteral(resourceName: "Liftoff"),
				title: "Liftoff",
				subtitle1: "Time: " + liftoff,
				subtitle2: "Rocket: \(launch.rocket)")
			Row(image:  imageLiteral(resourceName: "Loading"),
				title: "Propellant Loading",
				subtitle1: "Time: " + propellerLoading,
				subtitle2: "Location: \(launch.site)")
		}
		.navigationBarTitle("Launch details")
	}
}

struct LaunchDetailView_Previews: PreviewProvider {
	static var previews: some View {
		Group {
			NavigationView {
				DetailView(launch: TestData.launches[0])
			}
			Header(launch: TestData.launches[0])
				.padding()
				.previewLayout(.sizeThatFits)
		}
	}
}

detail view

Now, the important lesson.

Notice how all views are entirely detached from the global state of our app. They are not concerned with data storage of networking. All they do is show the data to the user.

This is not by chance.

In the MVC pattern, the role of views is only to display data and enable user interaction. All other responsibilities lie in the lower layers.

The app’s networking layer resides in controller objects

We now need to fetch the data from the API.

We can do that using a simple URLSession. My recommendation is to never use networking libraries like Alamofire, because they introduce many problems and have practically no benefit.

The first important thing to notice is that in an app connected to a remote API we don’t request only JSON data. Often, we need to download other kinds of resources, like images, videos or audio files.

So our network request code needs to be generic enough to handle any network request, and not only the ones that return JSON data.

class NetworkRequest {
	let url: URL
	
	init(url: URL) {
		self.url = url
	}
	
	func execute(withCompletion completion: @escaping (Data?) -> Void) {
		let task = URLSession.shared.dataTask(with: url, completionHandler: { (data: Data?, _, _) -> Void in
			DispatchQueue.main.async {
				completion(data)
			}
		})
		task.resume()
	}
}

As you can see, this code is minimal. There is no need to add a huge networking library to our project for something we can do in a few lines of code.

We now need a single source of truth for our app, i.e., a place where we put the data we download from the API. Given our app’s simple structure, we can hold the app’s state inside a single observable object.

class NetworkController: ObservableObject {
	@Published var launches: [Launch] = []
}

In a more complicated app, we might need to have more controllers to manage state, networking, and storage. In our simple app, this controller alone will suffice.

Nonetheless, state, storage, and networking are typical responsibilities of the controller layer.

To complete our app’s structure, we need an extra view that:

  • adds navigation between the LaunchesView and DetailView; and
  • connects our SwiftUI interface to the NetworkController.
struct ContentView: View {
	@EnvironmentObject private var networkController: NetworkController
	
	var body: some View {
		NavigationView {
			LaunchesView(launches: networkController.launches)
		}
	}
}

We create the NetworkController instance when the app launches.

At the time of writing, SwiftUI apps still use the UIKit app structure introduced in iOS 13. So we create our shared controllers in the SceneDelegate.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
	var window: UIWindow?
	
	func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
		if let windowScene = scene as? UIWindowScene {
			let window = UIWindow(windowScene: windowScene)
			let contentView = ContentView()
				.environmentObject(NetworkController())
			window.rootViewController = UIHostingController(rootView: contentView)
			self.window = window
			window.makeKeyAndVisible()
		}
}

If instead, you are using the new SwiftUI app structure introduced in iOS 14, you create a shared network controller instance as a @StateObject in your app’s main type.

@main
struct LaunchesApp: App {
	@StateObject private var networkController = NetworkController()
	
	var body: some Scene {
		WindowGroup {
			ContentView()
				.environmentObject(NetworkController())
		}
	}
}

This is the final structure of our app.

the final structure of the launches app

Performing network requests and decoding JSON data

We now need to fetch the data of a launch from the SpaceX API and decode it, to populate our views.

class NetworkController: ObservableObject {
	@Published var launches: [Launch] = []
	
	func fetchLaunches() {
		let url = URL(string: "https://api.spacexdata.com/v3/launches?limit=10")!
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			
		}
	}
}

When we receive a response with the JSON data, we need to decode it using a JSONDecoder, as we saw earlier in this article.

In a real app, I would create a whole taxonomy of network request to handle decoding. That requires the use of generics and would make things too complicated for this article, so we will decode the data in the view controller.

class NetworkController: ObservableObject {
	@Published var launches: [Launch] = []
	
	func fetchLaunches() {
		let url = URL(string: "https://api.spacexdata.com/v3/launches?limit=10")!
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			if let data = data {
				self?.decode(data)
			}
		}
	}
}

private extension NetworkController {
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		launches = (try? decoder.decode([Launch].self, from: data)) ?? []
	}
}

I usually put overrides, protocol conformance, and private methods in separate extensions, to keep my code well organized. You can put all the methods directly into the NetworkController if you prefer.

It’s a good practice to always use a weak self capture list for network request callbacks, to avoid leaking memory.

Keeping code modular to avoid callback hell when chaining network requests

In the above code for the NetworkController, I put the decoding of the data into a separate method.

This has many purposes:

  • It makes the controller’s code more modular and readable.
  • In the callback of the network request, we need to use a weak reference to self, for memory management considerations. Putting code in different methods allows us to avoid repeated optional unwrapping and binding.
  • Most importantly, it avoids callback hell.

Callback hell happens every time we need to sequence network requests that can start only when the previous one is completed.

It is rare for your apps only to perform one network request at a time. For example, after fetching some JSON data, you often need to follow all the URLs pointing to additional resources.

In our case, after fetching the list of launches, we need also to fetch their patch images.

private extension NetworkController {
	func decode(_ data: Data) {
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .formatted(DateFormatter.fullISO8601)
		launches = (try? decoder.decode([Launch].self, from: data)) ?? []
		for launch in launches {
			fetchPatch(for: launch)
		}
	}
	
	func fetchPatch(url: URL, launchId: Int) {
		let request = NetworkRequest(url: url)
		request.execute { [weak self] (data) in
			guard let data = data else { return }
			guard let index = self?.launches.firstIndex(where: { $0.id == launchId }) else { return }
			self?.launches[index].patch = UIImage(data: data)
		}
	}
}

Modular code is the simplest way to fix callback hell. Some developers use reactive frameworks like RxSwift or Combine, or complex solutions like futures and promises. But those are solutions I don’t like.

Instead, in complex controllers, I use an approach based on state machines, which requires another whole article. But in simple controllers like the one above, modularity is always my tool of choice.

All we have left to do, is fetch the data from the API when our app launches. There are several ways of achieving that. The most common one is to start a network request when the UI appears on screen.

struct ContentView: View {
	@EnvironmentObject private var networkController: NetworkController
	
	var body: some View {
		NavigationView {
			LaunchesView(launches: networkController.launches)
		}
		.onAppear(perform: networkController.fetchLaunches)
	}
}

Conclusions

Swift’s Codable protocols, paired with keyed and unkeyed decoding containers, allow for sophisticated decoding of data.

The most common data format to which these apply is JSON, but the Codable protocols work with any data format that has a hierarchical structure.

One such example is the property list format. For that, the iOS SDK offers the PropertyListEncoder and PropertyListDecoder classes. Everything we saw in this article still applies.

A plist file contains XML data, which, technically, is not restricted to hierarchical data. But the property list format has additional restrictions that make the Codable approach possible.

For full XML data, you need to use the XMLParser class instead. Unfortunately, decoding XML is not as straightforward as what we saw in this article.

Finally, the nitty-gritty details we saw in this article are not the only important aspect of decoding data. Correctly structuring the code of your apps is always crucial.

For more details and examples on how to structure a SwiftUI app, get my free guide below.

Architecting SwiftUI apps with MVC and MVVM

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

16 thoughts on “JSON Decoding in Swift with Codable: A Practical Guide”

  1. What if I’m using Core data? Do I still need to make a model(struct) file when my entities and attributes are essentially my model? How do I go about it? Thanks.

    Reply
    • The quick answer is that you can use the same approach with the managed objects of Core Data. The Codable protocols work with any type, including classes.

      The most complex answer requires a longer discussion about architecture, on wheter you should let managed objects spread around your code, or you should isolate Core Data behind a model controller that transforms them into structures.

      It’s a question I get sometimes. Hopefully I’ll get to write something about it someday.

      Reply
  2. Hi matteo, what a great and detailed tutorial for Codable using swift. You are my man! Keep posting such wonderful content. Long Live and cheers!

    Reply
  3. This is the best tutorial I have read about the JSON parsing. It emphasizes all the crucial points very clear and understandable way. Great job. Thanks Matteo…

    Reply
  4. Great Article, here you mean willDisplay.
    “When a cell appears, the table view calls tableView(_:cellForRowAt:) on its delegate. That’s where we perform network requests.”

    Reply
  5. You have the ability to read minds of the readers it feels. Wonderful and some really useful suggestions all along this amazing tutorial. I have personally avoided reading your post because it was a long one but it did not disappoint me once I read. I felt confident that I had the proper understanding of the concepts . Thanks Matteo.

    Reply
  6. Hello Matteo, Thanks for your always on point tutorials, recently i ran into a wall on an app that i am building with SwiftUI, I have a lot of Json data stored locally and my CodableBundleExtension can handle everything but as the application gets bigger with more pages and data it starts to crash because of memory usage. I have checked online for weeks on a way to solve the issue with no solution, please can you help pointing me to the right path on solving this issue?. Thanks.

    Reply
    • It’s hard for me to say what the right solution could be since it depends on how you app works and how your code is structured.

      My assumption is that you are storing everything in a single JSON file that is getting too big to decode. You probably don’t need all that data at once, so you should maybe split it across separate files that you can read independently.

      If you need anything more sophisticated than that, e.g., you need to query your data, then JSON is probably not the right solution. You need a database, so look into either Core Data or SQLite.

      Reply
      • Thanks for your reply, it is much appreciated. I already did try splitting acrossseparate json files, still didnt work. On taking on your advice i have started looking into Sqlite and Core Data, will keep you updated on my progress. Thanks a million Matteo.

        Reply
  7. The Codable stuff for custom parsing is great. Very useful and well-written. Thanks!

    One thing isn’t covered, though, as far as I can tell: How to handle nulls in the structure that’s being decoded. For example, doing this in init(from: Decoder):

    missionName = try container.decode(String.self, forKey: .missionName)

    Causes the entire init to fail if missionName is null. This will be very common, as databases are often full of columns that permit null.

    Reply

Leave a Comment