Decoding and Flattening Nested JSON with Codable

JSON data is rarely flat and often contains nested objects.

Nested JSON data can be decoded in Swift using the Decodable protocol and the JSONDecoder class.

However, how you decode nested JSON objects depends on the final result you want to obtain.



FREE GUIDE: Parsing JSON in Swift - The Ultimate Cheat Sheet

Learn the fundamentals of JSON parsing in Swift, advanced decoding techniques, and tasks for app development

DOWNLOAD THE FREE GUIDE

Table of contents

Decoding nested JSON objects using nested Swift types

The easiest way to decode nested JSON objects is to create equivalent Decodable Swift types that follow the nesting of the JSON data.

This is the most straightforward approach, and it works with single JSON objects and arrays.

import Foundation

let json = """
{
	"title": "Inception",
	"year": 2010,
	"director": {
		"name": "Christopher",
		"surname": "Nolan"
	},
	"cast": [
		{
			"name": "Leonardo",
			"surname": "DiCaprio"
		},
		{
			"name": "Cillian",
			"surname": "Murphy"
		},
		{
			"name": "Michael",
			"surname": "Caine"
		}
	]
}
""".data(using: .utf8)!

struct Movie: Decodable {
	let title: String
	let year: Int
	let director: Person
	let cast: [Person]
}

struct Person: Decodable {
	let name: String
	let surname: String
}

In the example above, the Person structure decodes the data of a movie’s director and cast.

Flattening single JSON objects using a nested keyed container

The standard approach may produce several Swift types that are not needed by your app and are only required for decoding the JSON data.

In such cases, it is better to flatten the nested JSON objects into a single Swift type containing all the needed values. For that, you need a decoding initializer with custom flattening logic.

In a decoding initializer, you decode the properties of a JSON object individually using a KeyedDecodingContainer keyed by your type’s coding keys. You get this container from the decoder by calling its nestedContainer(keyedBy:forKey:) method.

Similarly, you can obtain another KeyedDecodingContainer from the primary container to decode a nested JSON object by calling its nestedContainer(keyedBy:forKey:). This allows you to avoid creating full-fledged nested Swift type.

For example, our app might display the movie director’s name as a single String, so there is no need to declare a Person type for the director property of the Movie structure.

struct Movie: Decodable {
	let title: String
	let year: Int
	let director: String
	let cast: [Person]

	enum CodingKeys: CodingKey {
		case title
		case year
		case director
		case cast
	}

	enum PersonKeys: CodingKey {
		case name
		case surname
	}

	init(from decoder: any Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		self.title = try container.decode(String.self, forKey: .title)
		self.year = try container.decode(Int.self, forKey: .year)
		self.cast = try container.decode([Person].self, forKey: .cast)

		let directorContainer = try container.nestedContainer(
			keyedBy: PersonKeys.self,
			forKey: .director
		)
		let name = try directorContainer.decode(String.self, forKey: .name)
		let surname = try directorContainer.decode(String.self, forKey: .surname)
		self.director = name + " " + surname
	}
}

Notice that you must also create coding keys for the nested JSON object.

You also use a KeyedDecodingContainer to decode JSON with dynamic keys. However, in that case, you also need to complement it with a custom coding key structure.

Flattening arrays of JSON objects using a nested unkeyed container

The KeyedDecodingContainer works only for JSON objects, which can be keyed using coding keys. That does not work for arrays, so you must use an UnkeyedDecodingContainer instead.

Similarly to the code above, you get an UnkeyedDecodingContainer for a nested array of objects by calling the nestedUnkeyedContainer(forKey:) method on the main container.

Then, you iterate over the JSON objects inside the array and decode them using another KeyedDecodingContainer, which you get from the UnkeyedDecodingContainer.

For example, we can use this approach to flatten the cast’s names inside our Movie structure into a simple array of strings instead of an array of Person values.

struct Movie: Decodable {
	let title: String
	let year: Int
	let director: String
	let cast: [String]

	enum CodingKeys: CodingKey {
		case title
		case year
		case director
		case cast
	}

	enum PersonKeys: CodingKey {
		case name
		case surname
	}

	init(from decoder: any Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		// ...

		var castContainer = try container.nestedUnkeyedContainer(forKey: .cast)
		var cast: [String] = []
		while !castContainer.isAtEnd {
			let actorContainer = try castContainer.nestedContainer(keyedBy: PersonKeys.self)
			let name = try actorContainer.decode(String.self, forKey: .name)
			let surname = try actorContainer.decode(String.self, forKey: .surname)
			cast.append(name + " " + surname)
		}
		self.cast = cast
	}
}

Notice that the UnkeyedDecodingContainer advances to the next object inside the array every time we call its nestedContainer(keyedBy:forKey:) method. All we need to do is to check its isAtEnd property in a while loop to detect when the iteration has reached the end of the array.

You use an UnkeyedDecodingContainer also when iterating over arrays of JSON objects with dynamic types. However, in that case, you need to first use a KeyedDecodingContainer to read the dynamic type of the object you are decoding.

If you want a quick reference of how to flatten nested JSON data in Swift, together with other JSON parsing techniques, download my free cheat sheet below.

FREE GUIDE: Parsing JSON in Swift - The Ultimate Cheat Sheet

Learn the fundamentals of JSON parsing in Swift, advanced decoding techniques, and tasks for app development

DOWNLOAD THE FREE GUIDE

Leave a Comment