-1

I have this JSON format:

{
  "version":"7.0.19",
  "fields": ["ID","pm","age","pm_0","pm_1","pm_2","pm_3","pm_4","pm_5","pm_6","conf","pm1","pm_10","p1","p2","p3","p4","p5","p6","Humidity","Temperature","Pressure","Elevation","Type","Label","Lat","Lon","Icon","isOwner","Flags","Voc","Ozone1","Adc","CH"],
  "data":[[20,0.0,1,0.0,0.0,0.0,0.0,0.0,0.0,0.0,97,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,null,null,null,1413,0,"Oakdale",40.603077,-111.83612,0,0,0,null,null,0.01,1]],
  "count":11880
}

but I cannot work out how to use a Codable protocol to parse the json response.

this would be my desired model.

struct Point: Codable {
    let pm2: String?
    let latitude, longitude: Double?
    let temp: String?
    let iD: String?
    enum CodingKeys: String, CodingKey {
        case pm2 = "pm", temp = "Temperature", iD = "ID", latitude = "Lat", longitude = "Lon"
    }
}

Here is a URL to the json

https://webbfoot.com/dataa.json

matarali
  • 103
  • 2
  • 8
  • So, the idea is that the order of values in `"data"` is defined by the order of fields in `"fields"`? – New Dev Sep 29 '20 at 16:02
  • Yes. for example..The first row I.D. = 20, pm = 0.0 etc... – matarali Sep 29 '20 at 16:05
  • You should add to your question what the resulting model you want to get, even if you don't know how to do it – New Dev Sep 29 '20 at 16:07
  • I've answered a similar question in the past, so it might help: https://stackoverflow.com/a/61854827/968155 – New Dev Sep 29 '20 at 16:08
  • Blame the owner of the service for sending this pseudo-CSV format. This is extremely nonpractical to parse. – vadian Sep 29 '20 at 18:37

2 Answers2

0

You can use Codable to parse this:

struct Response: Decodable {

   let version: String
   let fields: [String]
   let data: [[QuantumValue?]]
   let count: Int

}
enter code here

enum QuantumValue: Decodable {

case float(Float), string(String)

init(from decoder: Decoder) throws {
    if let int = try? decoder.singleValueContainer().decode(Float.self) {
        self = .float(float)
        return
    }

    if let string = try? decoder.singleValueContainer().decode(String.self) {
        self = .string(string)
        return
    }

    throw QuantumError.missingValue
}

enum QuantumError:Error {
    case missingValue
}
}

QuantumValue will handle both Float and String and ? will handle the null part.

Rob
  • 2,086
  • 18
  • 25
  • This wouldn't work since `"data"` contains both `Int` and `String` and `null` – New Dev Sep 29 '20 at 16:05
  • 1
    That works, but you should clarify first with the OP what the resulting model should be. EDIT: see his latest edit – New Dev Sep 29 '20 at 16:12
0

This one is tricky and requires manual decoding. The principle would be to define a mapping between the fields you expect to decode and the properties of your object, then depending on the type of the property (String, Double, etc...), attempt to decode the data.

Second, since you have an array of points, you need some kind of container object to hold the array, for example:

struct Points {
    var data: [Point] = []
}

First, some of your model properties don't match the type in the data, e.g. iD is a String, but the data has an Int. For simplicity, I'll redefine your model to match the data

struct Point {
    var pm2: Int? = nil
    var latitude: Double? = nil
    var longitude: Double? = nil
    var temp: Int? = nil
    var iD: Int? = nil
}

Now, write the manual decoder for the parent container Points:

extension Points: Decodable {   
    static let mapping: [String: PartialKeyPath<Point>] = [
        "pm":          \Point.pm2,
        "Lat":         \Point.latitude,
        "Lon":         \Point.longitude,
        "Temperature": \Point.temp,
        "ID":          \Point.iD
    ]

    enum CodingKeys: CodingKey { case fields, data }
    private struct Dummy: Decodable {} // see below why

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        let fields = try container.decode([String].self, forKey: .fields)
        var data = try container.nestedUnkeyedContainer(forKey: .data)

        while !data.isAtEnd {
            var row = try data.nestedUnkeyedContainer()

            var point = Point()          
            for field in fields {

                let keyPath = Points.mapping[field]
                switch keyPath {
                case let kp as WritableKeyPath<Point, String?>:
                    point[keyPath: kp] = try row.decodeIfPresent(String.self)
                case let kp as WritableKeyPath<Point, Int?>:
                    point[keyPath: kp] = try row.decodeIfPresent(Int.self)
                case let kp as WritableKeyPath<Point, Double?>:
                    point[keyPath: kp] = try row.decodeIfPresent(Double.self)
                default:
                    // this is a hack to skip this value
                    let _ = try? row.decode(Dummy.self)
                }
            }
            self.data.append(point)
        }
    }
}

Once you have that, you can decode the JSON like so:

let points = try JSONDecoder().decode(Points.self, from: jsonData)
let firstPoint = points.data[0] 
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • I have added the URL to the question. Could you show me how this could work in your code above? Thanks – matarali Oct 02 '20 at 18:25
  • So, what you have in the URL is not quite what you had in your question. In the URL, the JSON contains multiple rows (inner arrays) of `data`, whereas the JSON in your question has a single inner row. Presumably, you'd want to decode an array of `Point`s? – New Dev Oct 02 '20 at 18:50
  • Yes.. i showed only one row in my initial question, but what I really want to do is find the ID of a particular row based on its lat long proximity to the users lat long – matarali Oct 02 '20 at 21:42
  • I updated the answer to decode an array of points. Once you decode the response, you can do whatever search you need. – New Dev Oct 02 '20 at 21:59