Search code examples
jsonswiftdecodable

Parsing different JSON feeds in Swift to the same Decodable struct


I have two different JSON feeds,

{
 "uid":9018823,
 "lat":43.25394,
 "lng":-2.93844,
 "bike":false,
 "name":"02-STATION",
 "address":null,
 "spot":true,
 "number":3388,
 "bikes":3,
 "booked_bikes":0,
 "bike_racks":20,
 "free_racks":16
}

and

{
 "address":null,"last_updated":1580546431,
 "renting":1,"returning":1,"uid":"3348"},
 "free_bikes":17,
 "id":"0a0d9d6e93abd05548c672b60bfa9099",
 "latitude":40.677236,
 "longitude":-74.015665,
 "station_name":"Coffey St & Conover St",
 "timestamp":"2020-02-01T09:26:31.254000Z"
}

What I would like is to parse both feeds filling the following structure,


struct Places: Codable {

    var name: String
    let lat: Double
    let lng: Double
}

As I've seen I can do this using Decodable in the init(from decoder:Decoder) but I can't wrap my head around it and make it work.


Solution

  • One way to solve this is to have a CodingKey enum for each json message type and then when trying to create a container using one enum we catch any error instead of throwing it and tries to create a container using the other enum

    struct Places: Decodable {
        let name: String
        let lat: Double
        let lng: Double
    
        enum CodingKeys1: String, CodingKey {
            case name
            case lat
            case lng
        }
    
        enum CodingKeys2: String, CodingKey {
            case name = "station_name"
            case lat = "latitude"
            case lng = "longitude"
        }
    
        init(from decoder: Decoder) throws {
            do {
                let container = try decoder.container(keyedBy: CodingKeys1.self)
                try self.init(container)
            } catch {
                let container = try decoder.container(keyedBy: CodingKeys2.self)
                try self.init(container)
            }
        }
    
        private init(_ container: KeyedDecodingContainer<CodingKeys1>) throws {
            name = try container.decode(String.self, forKey: .name)
            lat = try container.decode(Double.self, forKey: .lat)
            lng = try container.decode(Double.self, forKey: .lng)
        }
    
        private init(_ container: KeyedDecodingContainer<CodingKeys2>) throws {
            name = try container.decode(String.self, forKey: .name)
            lat = try container.decode(Double.self, forKey: .lat)
            lng = try container.decode(Double.self, forKey: .lng)
        }
    }
    

    Maybe the last part can be rewritten using generics.

    Here is an example where data1 and data2 are the two samples from the question

    do {
    
        for data in [data1, data2] {
            let result = try JSONDecoder().decode(Places.self, from: data)
            print(result)
        }
    } catch {
        print(error)
    }
    

    Places(name: "02-STATION", lat: 43.25394, lng: -2.93844)
    Places(name: "Coffey St & Conover St", lat: 40.677236, lng: -74.015665)