2

I have problem with decoding JSON. I am trying to decode my JSON with

let temp = try JSONDecoder().decode([LastTemperatureResponse].self, from: data).

My Codable structs is following:

struct LastTemperatureResponseElement: Codable {
    let measurement: Measurement
}

struct Measurement: Codable {
    let ts: String
    let sensors: [VportSensor]
}

struct VportSensor: TemperatureSensor, Codable {
    var lastUpdate: String!

    let address, description: String
    let status: String
    let temperature: Double
} 

Well, if I'm trying to decode my JSON, I am getting error message where it's quite clear

keyNotFound(CodingKeys(stringValue: "status", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "measurement", intValue: nil), CodingKeys(stringValue: "sensors", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"status\", intValue: nil) (\"status\").", underlyingError: nil))

but please take a look on my JSON

[
  {
    "type": "temperatures",
    "ts": "2017-11-08T16:43:59.558Z",
    "source": "thermo-king",
    "unit": {
      "number": "1226000743"
    },
    "measurement": {
      "ts": "2017-11-08T16:43:18.000Z",
      "sensors": [
        {
          "address": "t1",
          "description": "LFTest1",
          "setpoints": [
            {
              "address": "s1",
              "name": "LFSTest1"
            }
          ]
        },
        {
          "address": "t2",
          "description": "LFTest2",
          "setpoints": [
            {
              "address": "s2",
              "name": "LFSTest2"
            }
          ]
        },
        {
          "address": "t3",
          "description": "LFTest3",
          "setpoints": [
            {
              "address": "s3",
              "name": "LFSTest3"
            }
          ]
        },
        {
          "address": "t4",
          "description": "LFTest4"
        },
        {
          "address": "t5",
          "description": "LFTest5"
        },
        {
          "address": "t6",
          "description": "LFTest6"
        }
      ],
      "sensor": {
        "address": "t1",
        "name": "LFTest1"
      },
      "setpoints": [
        {
          "address": "s1",
          "name": "LFSTest1"
        }
      ]
    }
  },
  {
    "type": "temperatures",
    "ts": "2018-06-07T07:05:38.962Z",
    "source": "1-wire",
    "unit": {
      "number": "1226000743"
    },
    "measurement": {
      "ts": "2018-06-07T07:05:31.000Z",
      "sensors": [
        {
          "address": "2839A5B104000004",
          "description": "1-wire #1",
          "status": "ok",
          "temperature": 24.8
        },
        {
          "address": "28EFBAB104000061",
          "description": "1-wire #3",
          "status": "ok",
          "temperature": 24.5
        },
        {
          "address": "2845F6B504000034",
          "description": "1-wire #2",
          "status": "ok",
          "temperature": 24.5
        }
      ],
      "sensor": {
        "address": "2839A5B104000004",
        "name": "1-wire #1",
        "status": "ok"
      },
      "temperature": 24.8
    }
  },
  {
    "type": "temperatures",
    "ts": "2018-06-07T07:11:50.030Z",
    "source": "vport",
    "unit": {
      "number": "1226000743"
    },
    "measurement": {
      "ts": "2018-06-07T07:11:47.000Z",
      "sensors": [
        {
          "address": "1036040010",
          "description": "Vport 1-wire",
          "status": "high",
          "temperature": 26
        }
      ],
      "sensor": {
        "address": "1036040010",
        "name": "Vport 1-wire",
        "status": "high"
      },
      "temperature": 26
    }
  }
]

So I can guess that is giving error because of first portion of data, but should it be omitted and data generated with the rest?

Micgal
  • 133
  • 1
  • 1
  • 11

2 Answers2

6

After tracing your issue, I figured that there is couple of issues, First of all:

You are NOT declaring optionals:

based on the attached json, it seems that there are some of the properties that do not always exist, such as:

  • status => VportSensor.
  • temperature => Measurement.
  • temperature => VportSensor.
  • temperature => setpoints.

you would need make sure to declare any property that may not received as optional.

Also, The implementation of the Codeable structs:

the implemented structs seem to be not typical to the json response structure, make sure to declare your codable structs to be matched with the received json structure.


Note That:

  • lastUpdate and description are not used in VportSensor.
  • Based on my answer, there is no need to TemperatureSensor...

Tip:

When it comes to working with dates (such as ts), you should declare it directly as Date instead of String and then set the convenient dateDecodingStrategy. In your case, it should be a custom one, you could find how to do it in this answer.


Implementation:

Based on the above description, there is the full implementation:

struct Main: Codable {
    let type: String
    let ts: Date
    let source: String
    let unit: Unit
    let measurement: Measurement
}

struct Unit: Codable {
    var number: String
}

struct Measurement: Codable {
    let ts: String
    let sensors: [VportSensor]
    let sensor: VportSensor

    let temperature: Double?
}

struct LastTemperatureResponseElement: Codable {
    let measurement: Measurement
}

struct VportSensor: Codable {
    //let lastUpdate: String!
    //let description: String

    let address: String
    let name: String?
    let status: String?
    let temperature: Double?
    let setpoints: [Setpoint]?
}

struct Setpoint: Codable {
    let address: String
    let name: String
}

// this part from the mentioned answer for creating custom `dateDecodingStrategy`:
enum DateError: String, Error {
    case invalidDate
}

let decoder = JSONDecoder()

decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
    let container = try decoder.singleValueContainer()
    let dateStr = try container.decode(String.self)

    let formatter = DateFormatter()
    formatter.calendar = Calendar(identifier: .iso8601)
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.timeZone = TimeZone(secondsFromGMT: 0)
    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
    if let date = formatter.date(from: dateStr) {
        return date
    }
    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
    if let date = formatter.date(from: dateStr) {
        return date
    }
    throw DateError.invalidDate
})

Output:

let decoder = JSONDecoder()
do {
    let temp = try decoder.decode([Main].self, from: json)
    // here we go, `temp` is an array of main object of the json
} catch {
    print(error)
}

If you are wondering what is json in the

let temp = try decoder.decode([Main].self, from: json)

I just took the attached json response and add it into Data Object:

let json = """
[
  {
    "type": "temperatures",
    "ts": "2017-11-08T16:43:59.558Z",
    "source": "thermo-king",
    "unit": {
      "number": "1226000743"
    },
    "measurement": {
      "ts": "2017-11-08T16:43:18.000Z",
      "sensors": [
        {
          "address": "t1",
          "description": "LFTest1",
          "setpoints": [
            {
              "address": "s1",
              "name": "LFSTest1"
            }
          ]
        },
        {
          "address": "t2",
          "description": "LFTest2",
          "setpoints": [
            {
              "address": "s2",
              "name": "LFSTest2"
            }
          ]
        },
        {
          "address": "t3",
          "description": "LFTest3",
          "setpoints": [
            {
              "address": "s3",
              "name": "LFSTest3"
            }
          ]
        },
        {
          "address": "t4",
          "description": "LFTest4"
        },
        {
          "address": "t5",
          "description": "LFTest5"
        },
        {
          "address": "t6",
          "description": "LFTest6"
        }
      ],
      "sensor": {
        "address": "t1",
        "name": "LFTest1"
      },
      "setpoints": [
        {
          "address": "s1",
          "name": "LFSTest1"
        }
      ]
    }
  },
  {
    "type": "temperatures",
    "ts": "2018-06-07T07:05:38.962Z",
    "source": "1-wire",
    "unit": {
      "number": "1226000743"
    },
    "measurement": {
      "ts": "2018-06-07T07:05:31.000Z",
      "sensors": [
        {
          "address": "2839A5B104000004",
          "description": "1-wire #1",
          "status": "ok",
          "temperature": 24.8
        },
        {
          "address": "28EFBAB104000061",
          "description": "1-wire #3",
          "status": "ok",
          "temperature": 24.5
        },
        {
          "address": "2845F6B504000034",
          "description": "1-wire #2",
          "status": "ok",
          "temperature": 24.5
        }
      ],
      "sensor": {
        "address": "2839A5B104000004",
        "name": "1-wire #1",
        "status": "ok"
      },
      "temperature": 24.8
    }
  },
  {
    "type": "temperatures",
    "ts": "2018-06-07T07:11:50.030Z",
    "source": "vport",
    "unit": {
      "number": "1226000743"
    },
    "measurement": {
      "ts": "2018-06-07T07:11:47.000Z",
      "sensors": [
        {
          "address": "1036040010",
          "description": "Vport 1-wire",
          "status": "high",
          "temperature": 26
        }
      ],
      "sensor": {
        "address": "1036040010",
        "name": "Vport 1-wire",
        "status": "high"
      },
      "temperature": 26
    }
  }
]
""".data(using: .utf8)!
Ahmad F
  • 30,560
  • 17
  • 97
  • 143
  • Thanks for full implementation, however I have designed struct as needed, so for example, unit, "outside" ts, type are not necessary, so there is no need to implement them in struct. Lastupdate is necessary to calculate later. TemperatureSensor protocol is necessary, as there will be other structs which needs the same base. Setpoints are not considered also. The Date is good advice – Micgal Jun 07 '18 at 09:46
  • @Micgal so it depends on what you would need :) Probably the issue is not declaring optionals for the properties that don't always received. – Ahmad F Jun 07 '18 at 09:49
  • yep ;) seems it's solved with declaring as optionals, anyway thanks for your advice with Date – Micgal Jun 07 '18 at 09:50
1

You can easily skip the keys those are not getting from server response.

Example JSON response is:
{
    "isValid": false,
    "pendingAttempts": 2
} 

In this json response there is missing "id" field & in our code we have declared it. So we can easily skip it by the following code.

//Code example 

struct ResponseModel: Codable {

var id: String?    //misng in response
var isValid: Bool?
var token: String?

//initializer
init(id: String?, isValid: Bool?, token: String?) {
    self.id = id
    self.isValid = isValid
    self.token = token
}

//definging the coding keys
enum ResponseModelCodingKeys: String, CodingKey {

    //The right hand side keys should be same as of json response keys
    case id         = "id"
    case isValid    = "isValid"
    case token      = "token"
}

//decoding initializer
init(from decoder: Decoder) throws {

    var id: String?
    var isValid: Bool?
    var token: String?

    let container = try decoder.container(keyedBy: ResponseModelCodingKeys.self) // defining our (keyed) container
    do {
        //if found then map
        id = try container.decode(String.self, forKey: .id)
    }
    catch {
        //not found then just set the default value
        /******** This case will be executed **********/
        id = ""
    }

    do {
        //if found then map
        isValid = try container.decode(Bool.self, forKey: .isValid)
    }
    catch {
        //not found then just set the default value
        isValid = false
    }

    do {
        //if found then map
        token = try container.decode(String.self, forKey: .token)
    }
    catch {
        //not found then just set the default value
        token = ""
    }
    //Initializing the model
    self.init(id: id, isValid: isValid, token: token)
}
}

This technique is useful when we have common response for multiple API's & each API have some missing keys.

Naresh
  • 869
  • 8
  • 17