Decode JSON with Dynamic Keys in Swift [Dictionaries and Arrays]

JSON object with dynamic keys presents a specific challenge when decoding data in Swift because stored properties cannot represent dynamic keys.

However, it’s still possible to decode JSON with dynamic keys using the Decodable protocol into dictionaries or arrays by implementing structures that conform to the CodingKey protocol.



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

Most of the time, nested JSON objects can be decoded by creating Swift types that match their structure.

However, sometimes, a JSON object contains dynamic keys with unpredictable names. Such fields cannot be represented by stored properties inside a Swift type.

JSON data usually have dynamic keys when values are encoded using an ID as their key name. For example, take the following JSON data containing the weather reports for several cities.

{
	"Amsterdam": {
		"conditions": "Partly Cloudy",
		"temperature": 11
	},
	"New York": {
		"conditions": "Sunny",
		"temperature": 15,
	},
	"Moscow": {
		"conditions": "Cloudy",
		"temperature": 6,
	},
}

While you might often get collections of JSON objects inside a single array, in this example, the cities’ names are used as the key for their corresponding weather reports.

Decoding JSON objects with dynamic keys as key-value pairs in a Swift dictionary

JSON objects that use IDs as keys are often generated by data stored in dictionaries, generally known as hash tables or hash maps. Therefore, the simplest way to decode JSON data with dynamic keys is to transform it into a Swift dictionary.

The nested JSON objects require a Decodable Swift type. The parent object can instead be decoded as a dictionary with String or Int keys, depending on the kind of keys the JSON data uses.

This is straightforward because the Dictionary type conforms to the Decodable protocol whenever its keys and values do, too.

import Foundation

let json = """
{
	"Amsterdam": {
		"conditions": "Partly Cloudy",
		"temperature": 11
	},
	"New York": {
		"conditions": "Sunny",
		"temperature": 15,
	},
	"Moscow": {
		"conditions": "Cloudy",
		"temperature": 6,
	},
}
""".data(using: .utf8)!

struct WeatherReport: Decodable {
	let conditions: String
	let temperature: Int
}

let reports = try JSONDecoder().decode([String: WeatherReport].self, from: json)

No further steps are necessary if your app needs to access the decoded data as key-value pairs. Otherwise, you can loop through the dictionary and transform its content into whatever format you need.

For example, you can easily transform the dictionary into an array.

struct WeatherReport: Decodable {
	var city: String?
	let conditions: String
	let temperature: Int
}

let reports = try JSONDecoder().decode([String: WeatherReport].self, from: json)
var reportsArray: [WeatherReport] = []
for (city, var report) in reports {
	report.city = city
	reportsArray.append(report)
}

Using a CodingKey structure to decode dynamic keys as a Swift array

The dictionary approach I showed above forces you to make a value’s identifier optional and mutable, which is not a good practice.

A better approach is to decode the JSON data using dynamic coding keys and custom decoding initializers.

For starters, the nested JSON objects need a custom decoding initializer that takes its identifier from the coding path provided by the KeyedDecodingContainer.

struct WeatherReport: Decodable {
	let city: String
	let conditions: String
	let temperature: Int

	enum CodingKeys: CodingKey {
		case conditions
		case temperature
	}

	init(from decoder: any Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		self.city = container.codingPath.first!.stringValue
		self.conditions = try container.decode(String.self, forKey: .conditions)
		self.temperature = try container.decode(Int.self, forKey: .temperature)
	}
}

Now, the city property is not optional. It’s a let constant that cannot be changed after being initialized.

Creating a dynamic coding key structure conforming to the CodingKey protocol

Instead of decoding the main JSON object as a dictionary, we must create a Decodable Swift type. However, we cannot provide the typical CodingKeys enumeration since the keys in the JSON data are dynamic and could have any value.

The solution lies in the fact that coding keys do not have to be represented by a Swift enumeration.

The CodingKey protocol requires two failable initializers that can take either a String or an Int as parameters and two correspondingly typed stored properties. While the compiler automatically synthesizes these for coding key enumerations, we can implement them explicitly in any type.

This means we can create a CodingKey structure that can be initialized with a dynamic value while decoding the main JSON object instead of having a fixed one.

struct WeatherInfo: Decodable {
	let reports: [WeatherReport]

	struct CodingKeys: CodingKey {
		var stringValue: String
		var intValue: Int? = nil

		init?(stringValue: String) {
			self.stringValue = stringValue
		}

		init?(intValue: Int) {
			return nil
		}
	}
}

Decoding the JSON data into an array using a keyed decoding container with dynamic keys

In the custom decoding initializer for the WeatherInfo structure, we first create a KeyedDecodingContainer as usual. The difference, however, is that this contained is keyed using the dynamic CodingKey structure instead of a static enumeration.

Then, we iterate through the container’s allKeys property, which returns all the keys in the JSON object as values of our CodingKey structure. We can use these keys to decode each WeatherReport value inside the loop as we typically do.

Since we are passing dynamic keys to the decode(_:forKey:) method of the KeyedDecodingContainer, the custom decoding initializer of the WeatherReport type will use that dynamic key to set its initializer during the decoding.

struct WeatherInfo: Decodable {
	let reports: [WeatherReport]

	struct CodingKeys: CodingKey {
		var stringValue: String
		var intValue: Int? = nil

		init?(stringValue: String) {
			self.stringValue = stringValue
		}

		init?(intValue: Int) {
			return nil
		}
	}

	init(from decoder: any Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		var reports: [WeatherReport] = []
		for key in container.allKeys {
			let report = try container.decode(WeatherReport.self, forKey: key)
			reports.append(report)
		}
		self.reports = reports
	}
}

let reports = try JSONDecoder().decode(WeatherInfo.self, from: json)

Notice that we do not need to add extra code when we parse the JSON data using a JSONDecoder instance. From the outside, the decoding happens transparently like for any other Decodable type.

If you want a quick reference of decoding JSON objects with dynamic keys, 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