Search code examples
arraysjsonswiftdecodecodable

Decode JSON array without keys in Swift


I make a request to the OpenSky Network API and get a result like this:

{
  "time":1629739170,
  "states":[
    ["4b1800","SWR1076 ","Switzerland",1629739169,1629739169,8.5493,47.4581,373.38,false,79.14,275.97,7.15,null,487.68,null,false,0],
    ["3c65c4","DLH35A  ","Germany",1629739170,1629739170,6.5185,46.2346,11590.02,false,255.57,48.84,0,null,11986.26,"1000",false,0]
  ]
}

Now I want to decode that JSON string to a list of FlightState objects, but there are no keys provided. From the API documentation (see link above) I know what value corresponds to what property, but how do I create corresponding Swift objects?

The following doesn't work, obviously, because there are no keys.

let decoder = JSONDecoder()             
let flightState: FlightState = try! decoder.decode(FlightState.self, from: dataString.data(using: .utf8)!)


struct FlightState: Codable {
    
    let time: Int
    let states: [StateVector]

    struct StateVector: Codable {
        let icao24: UUID
        let callsign: String
        let origin_country: String
        let time_position: Int
        let last_contact: Int
        let longitude: Float
        let latitude: Float
        let baro_altitude: Float
        let on_ground: Bool
        let velocity: Float
        let true_track: Float
        let vertical_rate: Float
        let sensors: [Int]
        let geo_altitude: Float
        let squawk: String
        let spi: Bool
        let position_source: Int
    }

}

The error message says Expected to decode Dictionary<String, Any> but found an array instead. So how do I tell Swift to interpret the values in the states lists as properties of objects with their corresponding type?

I have the feeling that someone must have been stumbled upon this before, but I couldn't find a solution. Maybe I am too new to Swift to recognize a solution as such.


Solution

  • You can decode a JSON where specific properties are held in specific array indices rather than under specific Dictionary keys by implementing init(from decoder:) on your own and using an unkeyedContainer() to decode the array, then calling decode to decode each property in order.

    Bear in mind lots of properties in the response can be null, so you need to declare those as Optional and use decodeIfPresent rather than decode.

    struct FlightState: Decodable {
    
        let time: Int
        let states: [StateVector]
    
        struct StateVector: Decodable {
            let icao24: String
            let callSign: String
            let originCountry: String
            let timePosition: Int?
            let lastContact: Int
            let longitude: Float?
            let latitude: Float?
            let baroAltitude: Float?
            let onGround: Bool
            let velocity: Float?
            let trueTrack: Float?
            let verticalRate: Float?
            let sensors: [Int]?
            let geoAltitude: Float?
            let squawk: String?
            let spi: Bool
            let positionSource: Int
    
            init(from decoder: Decoder) throws {
                var values = try decoder.unkeyedContainer()
                self.icao24 = try values.decode(String.self)
                self.callSign = try values.decode(String.self)
                self.originCountry = try values.decode(String.self)
                self.timePosition = try values.decodeIfPresent(Int.self)
                self.lastContact = try values.decode(Int.self)
                self.longitude = try values.decodeIfPresent(Float.self)
                self.latitude = try values.decodeIfPresent(Float.self)
                self.baroAltitude = try values.decodeIfPresent(Float.self)
                self.onGround = try values.decode(Bool.self)
                self.velocity = try values.decodeIfPresent(Float.self)
                self.trueTrack = try values.decodeIfPresent(Float.self)
                self.verticalRate = try values.decodeIfPresent(Float.self)
                self.sensors = try values.decodeIfPresent([Int].self)
                self.geoAltitude = try values.decodeIfPresent(Float.self)
                self.squawk = try values.decodeIfPresent(String.self)
                self.spi = try values.decode(Bool.self)
                self.positionSource = try values.decode(Int.self)
            }
        }
    }