sorry if the question is vague but I am trying to be as expressive as possible.
I have the following model:
struct Posts: Codable, Identifiable {
let id: String
let title: String
let content: String
enum CodingKeys: String, CodingKey {
case id = "_id"
case title
case content
}
}
the server response if a post is found would be the same model no issues because the JSON matches the model .
but if the server returns an error post not found, this would be the response JSON:
{
"error": "No records found"
}
I receive the following when this happens:
keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "id", intValue: nil) ("id").", underlyingError: nil))
what would be the best approach to handle this issue?
UPDATE:
Thank you jnpdx!
So, I did a ErrorResponse Struct, and it did catch the error response like so:
struct ErrorResponse: Codable {
let error: String
enum CodingKeys: String, CodingKey {
case error
}
}
So in my APIServices file, how do I handle this?
// this is what gets the Post data
let decodedData = try JSONDecoder().decode(Post?.self, from: data)
//Do I need another JSONDecoder to also catch the error below the above line like this?
let decodedDataError = try JSONDecoder().decode(ErrorResponse?.self, from: data)
In the comments, we discussed creating a struct to model the error, which it looks like you've done. To address your followup, no, you don't need a separate JSONDecoder. You also probably should not be decoding optionals.
There's not just a single way to do this correctly, but your function might look something like this:
let decoder = JSONDecoder()
do {
let post = try decoder.decode(Post.self, from: data)
//handle the post
} catch {
//try to decode an error
if let error = try? decoder.decode(ErrorResponse.self, from: data) {
//handle an API error
}
//handle an unknown error
}
Related
I'm new to Swift 5.3 and having trouble retrieving my nested JSON data.
My JSON data result looks like this:
{
"sites":[
{
"site_no":"16103000",
"station_nm":"Hanalei River nr Hanalei, Kauai, HI",
"dec_lat_va":22.1796,
"dec_long_va":-159.466,
"huc_cd":"20070000",
"tz_cd":"HST",
"flow":92.8,
"flow_unit":"cfs",
"flow_dt":"2020-08-18 07:10:00",
"stage":1.47,
"stage_unit":"ft",
"stage_dt":"2020-08-18 07:10:00",
"class":0,
"percentile":31.9,
"percent_median":"86.73",
"percent_mean":"50.77",
"url":"https:\/\/waterdata.usgs.gov\/hi\/nwis\/uv?site_no=16103000"
}
]
}
My structs look like this:
struct APIResponse: Codable {
let sites: APIResponseSites
}
struct APIResponseSites: Codable {
let station_nm: String
let stage: Float
}
And my Decode SWIFT looks like this:
let task = URLSession.shared.dataTask(with: url, completionHandler: {
data, _, error in
guard let data = data, error == nil else {
return
}
var result: APIResponse?
do {
result = try JSONDecoder().decode(APIResponse.self, from: data)
}
catch {
print("Failed to decode with error: \(error)")
}
guard let final = result else {
return
}
print(final.sites.station_nm)
print(final.sites.stage)
})
And of course, I get an error that states:
Failed to decode with error:
typeMismatch(Swift.Dictionary<Swift.String, Any>,
Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue:
"sites", intValue: nil)], debugDescription: "Expected to decode
Dictionary<String, Any> but found an array instead.", underlyingError:
nil))
I know it has to do with 'sites' returning an array (a single one) but I don't know how to fix it. Any help would be greatly appreciated.
The error message it is pretty clear you need to parse an array of objects instead of a single object.
Just change your root declaration property from
let sites: APIResponseSites
to
let sites: [APIResponseSites]
**1.** First "sites" is an array so replace
let sites: APIResponseSites
with
let sites: [APIResponseSites]()
**2.** As sites is a array collection, please print value like given below:
print(final.sites.first?.station_nm ?? "")
print(final.sites.first?.stage ?? 0.0)
Final code is here:
struct APIResponse: Codable {
let sites: [APIResponseSites]()
}
struct APIResponseSites: Codable {
let station_nm: String
let stage: Float
}
let task = URLSession.shared.dataTask(with: url, completionHandler: {
data, _, error in
guard let data = data, error == nil else {
return
}
var result: APIResponse?
do {
result = try JSONDecoder().decode(APIResponse.self, from: data)
}
catch {
print("Failed to decode with error: \(error)")
}
guard let final = result else {
return
}
print(final.sites.first?.station_nm ?? "")
print(final.sites.first?.stage ?? 0.0)
})
I'm pretty sure my model is correct based on my data, I cannot figure out why I am getting the format error?
JSON:
{
"1596193200":{
"clientref":1,
"type":"breakfast"
},
"1596200400":{
"clientref":0,
"type":"lunch"
},
"1596218400":{
"clientref":2,
"type":"dinner"
}
}
model:
struct Call: Decodable {
let clientref: Int?
let type: String?
}
edit updated question with the code for decoding the json data from the URL:
class CallService {
static let shared = CallService()
let CALLS_URL = "url.com/Calls.json"
func fetchCalls(completion: #escaping ([Call]) -> ()) {
guard let url = URL(string: CALLS_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
print("Failed to fetch data with error: ", error.localizedDescription)
return
}
guard let data = data else {return}
do {
let call = try JSONDecoder().decode([Call].self, from: data)
completion(call)
} catch let error {
print("Failed to create JSON with error: ", error.localizedDescription)
}
}.resume()
}
}
I strongly suggest to learn how to debug: it includes where to look, what info to get, where to get them, etc, and at the end, fix it.
That's a good thing that you print the error, most beginner don't.
print("Failed to create JSON with error: ", error.localizedDescription)
=>
print("Failed to create JSON with error: ", error)
You'll get a better idea.
Second, if it failed, print the data stringified. You're supposed to have JSON, that's right. But how often do I see question about that issue, when it fact, the answer wasn't JSON at all (the API never stated it will return JSON), the author were facing an error (custom 404, etc.) and did get a XML/HTML message error etc.
So, when the parsing fails, I suggest to do:
print("Failed with data: \(String(data: data, encoding: .utf8))")
Check that the output is a valid JSON (plenty of online validators or apps that do that).
Now:
I'm pretty sure my model is correct based on my data,
Well, yes and no.
Little tip with Codable when debuting (and not using nested stuff): Do the reverse.
Make your struct Codable if it's not the case yet (I used Playgrounds)
struct Call: Codable {
let clientref: Int?
let type: String?
}
do {
let calls: [Call] = [Call(clientref: 1, type: "breakfast"),
Call(clientref: 0, type: "lunch"),
Call(clientref: 2, type: "dinner")]
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
let jsonData = try encoder.encode(calls)
let jsonStringified = String(data: jsonData, encoding: .utf8)
if let string = jsonStringified {
print(string)
}
} catch {
print("Got error: \(error)")
}
Output:
[
{
"clientref" : 1,
"type" : "breakfast"
},
{
"clientref" : 0,
"type" : "lunch"
},
{
"clientref" : 2,
"type" : "dinner"
}
]
It doesn't look like. I could only used an array to put various calls inside a single variable, and that's what you meant for decoding, because you wrote [Call].self, so you were expecting an array of Call. We are missing the "1596218400" parts. Wait, could it be a dictionary at top level? Yes. You can see the {} and the fact it uses "keys", not listing one after the others...
Wait, but now that we printed the full error, does it make more sense now?
typeMismatch(Swift.Array<Any>,
Swift.DecodingError.Context(codingPath: [],
debugDescription: "Expected to decode Array<Any> but found a dictionary instead.",
underlyingError: nil))
Fix:
let dictionary = try JSONDecoder().decode([String: Call].self, from: data)
completion(dictionary.values) //since I guess you only want the Call objects, not the keys with the numbers.
From the code you provided it looks like you are trying to decode an Array<Call>, but in the JSON the data is formatted as a Dictionary<String: Call>.
You should try:
let call = try JsonDecoder().decode(Dictionary<String: Call>.self, from: data)
I am using NASA API in my iOS application for getting some images. My response from the server looks like:
{
"date": "2014-02-04T03:30:01",
"id": "LC8_L1T_TOA/LC81270592014035LGN00",
"resource": {
"dataset": "LC8_L1T_TOA",
"planet": "earth"
},
"service_version": "v1",
"url": "https://earthengine.googleapis.com/api/thumb?thumbid=bc77b079c8ecd07cd668c576c22b83a4&token=a16639b0d38dd68c586c24a6ee5299d9"
}
My request url is:
https://api.nasa.gov/planetary/earth/imagery/?lon=100.75&lat=1.5&date=2014-02-01&api_key=DEMO_KEY
My struct for decoding this response is:
import Foundation
// MARK: - EarthImages
struct EarthImages: Codable {
let date: String
let id: String
let resource: Resource
let serviceVersion: String
let url: String
private enum CodingKeys: String, CodingKey {
case date = "date"
case id = "id"
case resource = "resource"
case serviceVersion = "service_version"
case url = "url"
}
}
The problem is - when I am trying to decode my response using the following code
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
let earthImages = try JSONDecoder().decode(EarthImages.self, from: data)
print(earthImages.url)
}
catch let error{
print(error)
}}
}.resume()
I get in console.
keyNotFound(CodingKeys(stringValue: "date", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"date\", intValue: nil) (\"date\").", underlyingError: nil))
I used PAW to check if I get correct response, and it works, so the problem more likely is in my code. How can I resolve this issue?
It... looks fine? (Assuming Resource is also Decodable, but that would be a separate issue). Perhaps the error is actually telling you the truth, you may be attempting to decode a JSON blob that does not have a date value.
You can explicitly see what we're attempting to decode with an added print just before we attempt to decode:
if let data = data {
print(String(data: data, encoding: .utf8)!)
do {
...
Then separately, if the date field is not guaranteed to exist in every response, you should make it optional:
struct EarthImages: Codable {
let date: String?
let id: String
let resource: Resource
let serviceVersion: String
let url: String
}
Small note, string enums dont need to be redeclared if its exactly the same as the enum case:
enum CodingKeys: String, CodingKey {
case date // "date" is implied
case id
case resource
case serviceVersion = "service_version"
case url
}
Another fun fact: JSONDecoder can also convert from snake case automatically without having to define CodingKeys if every key is consistent.
So you can also do:
struct EarthImages: Codable {
let date: String
let id: String
let resource: Resource
let serviceVersion: String
let url: String
}
...
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let earthImages = try decoder.decode(EarthImages.self, from: data)
}
I am trying to decode the API from the API below, but I am still getting the error below:
keyNotFound(CodingKeys(stringValue: "resources", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "resources", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: \"resources\", intValue: nil) (\"resources\").", underlyingError: nil))
API Address: https://age-of-empires-2-api.herokuapp.com/docs/
I have already tried reviewing my structs numerous times, and trying the call code within different levels of the API, but still cannot get the data. I have also tried calling the objects from different levels with the print(resources.XXX) format.
This is my first data call from the API:
{
"resources": {
"civilizations": "https://age-of-empires-2-api.herokuapp.com/api/v1/civilizations",
"units": "https://age-of-empires-2-api.herokuapp.com/api/v1/units",
"structures": "https://age-of-empires-2-api.herokuapp.com/api/v1/structures",
"technologies": "https://age-of-empires-2-api.herokuapp.com/api/v1/technologies"
}
}
These are the first two levels of the structs:
// MARK: - Resources
struct Resources: Codable {
let resources: [String : ResourcesList]
enum CodingKeys: String, CodingKey {
case resources
}
}
// MARK: - ResourcesList
struct ResourcesList: Codable {
let civilizations: CivilizationList
let units: UnitList
let structures: StructureList
let technologies: TechnologyList
enum CodingKeys: String, CodingKey {
case civilizations, units, technologies, structures
}
}
Below these structs, I have implemented the models as indicated in the API website, e.g., CivilizationList, Civilization etc.
This is my call code:
let jsonUrl = "https://age-of-empires-2-api.herokuapp.com/api/v1"
guard let url = URL(string: jsonUrl) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
let dataAsString = String(data: data, encoding: .utf8)
do {
let decoder = JSONDecoder()
let resources = try decoder.decode([String : Resources].self, from: data)
print(resources)
} catch {
print(error)
}
print(dataAsString!)
}.resume()
I have reviewed all the other threads here about the same error code, tried stuff, but there is probably something very basic that I am missing, unfortunately I am too much of a beginner to notice it. Any help is appreciated.
Model should be
// MARK: - Empty
struct Resources: Codable {
let resources: ResourcesList
}
// MARK: - Resources
struct ResourcesList: Codable {
let civilizations, units, structures, technologies: String
}
Decode
let resources = try decoder.decode(Resources.self, from: data)
as civilizations, units, structures, technologies are strings not models
OR
let resources = try decoder.decode([String : ResourcesList].self, from: data)
I receive the following error:
Error serialising json typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary but found an array instead.", underlyingError: nil))
Code:
//---------------
struct Currency: Decodable {
let symbol: String
let price: String
}
var myDict: [Currency] = []
//---------------
func testParse(){
let jsonUrlString = "https://api.binance.com/api/v3/ticker/price"
guard let url = URL(string: jsonUrlString) else
{ return }
URLSession.shared.dataTask(with: url) { (data,response,err) in
guard let data = data else
{
print("Error: No data to decode")
return
}
do
{
let exchanges = try
JSONDecoder().decode(Currency.self, from: data)
let X: [Currency] = [exchanges]
self.myDict = X
self.testFunc()
print("binance: "+self.myDict[0].symbol + ": "+self.myDict[0].price)
}
catch let jsonErr
{
print("Error serialising json",jsonErr)
}
}
.resume()
}
Is the issue with my struct layout? Or would it be how I'm parsing? I'd like to get a better understanding here for future reference. Therefore, if anyone could link a good Swift 4 guide it would be greatly appreciated. Alternatively, if you could give me a detailed answer that would be great (rather than spoon feeding the answer where I don't learn).
Please read the error message carefully and learn to understand it. It's very clear.
Expected to decode Dictionary but found an array instead
In other words: You want to decode a dictionary (Currency) but in truth it's an array ([Currency]).
In terms of Decodable a dictionary is the target struct or class.
And please don't name an object as ...dict which is actually an array.
var myArray = [Currency]()
...
let exchanges = try JSONDecoder().decode([Currency].self, from: data)
self.myArray = exchanges