Decode a nested JSON structure with Swift Decodable
In a Json file, properties are often nested. This generates a longer or shorter path to the value.
The access to this value can be direct with the use of containers.
Example of a JSON file obtained with the OpenWeather API :
{
"coord": {
"lon": -0.1257,
"lat": 51.5085
},
"weather": [
{
"id": 804,
"main": "Clouds",
"description": "couvert",
"icon": "04d"
}
],
"base": "stations",
"main": {
"temp": 14.41,
"feels_like": 13.84,
"temp_min": 12.61,
"temp_max": 15.32,
"pressure": 1018,
"humidity": 74
},
"visibility": 10000,
"wind": {
"speed": 5.66,
"deg": 210
},
"clouds": {
"all": 99
},
"dt": 1641035334,
"sys": {
"type": 2,
"id": 2019646,
"country": "GB",
"sunrise": 1641024368,
"sunset": 1641052893
},
"timezone": 0,
"id": 2643743,
"name": "Londres",
"cod": 200
}
Use a tool to format JSON in Swift
Examples of tools :
The result is :
import Foundation
// MARK: - Welcome2
struct Welcome2 {
let coord: Coord
let weather: [Weather]
let base: String
let main: Main
let visibility: Int
let wind: Wind
let clouds: Clouds
let dt: Int
let sys: Sys
let timezone, id: Int
let name: String
let cod: Int
}
// MARK: - Clouds
struct Clouds {
let all: Int
}
// MARK: - Coord
struct Coord {
let lon, lat: Double
}
// MARK: - Main
struct Main {
let temp, feelsLike, tempMin, tempMax: Double
let pressure, humidity: Int
}
// MARK: - Sys
struct Sys {
let type, id: Int
let country: String
let sunrise, sunset: Int
}
// MARK: - Weather
struct Weather {
let id: Int
let main, weatherDescription, icon: String
}
// MARK: - Wind
struct Wind {
let speed: Double
let deg: Int
}
Beware of property name change ("feels_like" ==> "feelsLike")
The name of the structures is adjusted ("Welcome2" ==> "WeatherData").
Keep only the properties whose values will be used.
Customise the name of the properties and ensure correspondence with the keys used in Json, with CodingKeys. (Use "feels_like").
Add protocol Decodable.
The file becomes :
import Foundation
struct WeatherData: Decodable {
let city: String
let country: Country
let skyCondition: [SkyCondition]
let temperatureData: TemperatureData
enum CodingKeys: String, CodingKey {
case city = "name"
case country = "sys"
case skyCondition = "weather"
case temperatureData = "main"
}
}
struct SkyCondition: Decodable {
let description: String
let icon: String
}
struct TemperatureData: Decodable {
let temperature: Double
let feelsLike: Double
enum CodingKeys: String, CodingKey {
case temperature = "temp"
case feelsLike = "feels_like"
}
}
struct Country: Decodable {
let country: String
}
`
WeatherData can be decoded in a query.
The disadvantage is that the path to a nested value is long.
To access the value "temperature", the path would look like this weatherData.temperatureData.temperature
Hence the use of containers.
Use of container and nestedContainer
For data contained in an array, nestedUnkeyedContainer is used.
The nesting of data containers is represented in an enum.
The containers are created. Properties decoded.
import Foundation
struct WeatherData: Decodable {
enum MainKeys: String, CodingKey {
case skyCondition = "weather"
case temperatureData = "main"
case country = "sys"
case city = "name"
enum SkyConditionKeys: String, CodingKey {
case description
case icon
}
enum TemperaturesKeys: String, CodingKey {
case temperature = "temp"
case feelsLike = "feels_like"
}
enum CountryKeys: String, CodingKey {
case country
}
}
let description: String
let icon: String
let temperature: Double
let feelsLike: Double
let country: String
let city: String
init(from decoder: Decoder) throws {
// main container
let container = try decoder.container(keyedBy: MainKeys.self)
// container array with description and icon
var skyConditionContainer = try container.nestedUnkeyedContainer(forKey: .skyCondition)
let firstSkyContainer = try skyConditionContainer
.nestedContainer(keyedBy: MainKeys.SkyConditionKeys.self)
self.description = try firstSkyContainer.decode(String.self, forKey: .description)
self.icon = try firstSkyContainer.decode(String.self, forKey: .icon)
// container with temperature and feelslike
let temperatureContainer = try container
.nestedContainer(keyedBy: MainKeys.TemperaturesKeys.self, forKey: .temperatureData)
self.temperature = try temperatureContainer.decode(Double.self, forKey: .temperature)
self.feelsLike = try temperatureContainer.decode(Double.self, forKey: .feelsLike)
// container with country
let countryContainer = try container
.nestedContainer(keyedBy: MainKeys.CountryKeys.self, forKey: .country)
self.country = try countryContainer.decode(String.self, forKey: .country)
//name in main container
self.city = try container.decode(String.self, forKey: .city)
}
}
To access the value "temperature", the path is now weatherData.temperature
Another example of container nesting with the Google translate Json.
{
"data": {
"translations": [
{
"translatedText": "welcome to berlin"
}
]
}
}
The Swift file would look like :
import Foundation
struct TranslationResponse: Decodable {
enum CodingKeys: String, CodingKey {
case data
enum DataKeys: String, CodingKey {
case translations
enum TranslationsKeys: String, CodingKey {
case translatedText
}
}
}
let translatedText: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// data container
let dataContainer = try container
.nestedContainer(keyedBy: CodingKeys.DataKeys.self, forKey: .data)
// translations array container
var translateContainer = try dataContainer
.nestedUnkeyedContainer(forKey: .translations)
let firstTranslateContainer = try translateContainer
.nestedContainer(keyedBy: CodingKeys.DataKeys.TranslationsKeys.self)
self.translatedText = try firstTranslateContainer
.decode(String.self, forKey: .translatedText)
}
}
To access the value "translatedText", the path is translationResponse.translatedText
Thanks for reading
good coding
Thanks to Hamish from stackoverflow