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