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.
- This article is part of the Parsing JSON in Swift series.
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 GUIDETable 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 GUIDEMatteo has been developing apps for iOS since 2008. He has been teaching iOS development best practices to hundreds of students since 2015 and he is the developer of Vulcan, a macOS app to generate SwiftUI code. Before that he was a freelance iOS developer for small and big clients, including TomTom, Squla, Siilo, and Layar. Matteo got a master’s degree in computer science and computational logic at the University of Turin. In his spare time he dances and teaches tango.