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)
}
Related
I am running into an issue building the correct data model for the following JSON response.
{
"resources": [
{
"courseid": 4803,
"color": "Blue",
"teeboxtype": "Championship",
"slope": 121,
"rating": 71.4
},
{
"courseid": 4803,
"color": "White",
"teeboxtype": "Men's",
"slope": 120,
"rating": 69.6
},
{
"courseid": 4803,
"color": "Red",
"teeboxtype": "Women's",
"slope": 118,
"rating": 71.2
}
]
}
Here is the current model. No matter what I do I can't seem to get the model populated. Here is also my URL session retrieving the data. I am new to Swift and SwiftUI so please be gentle. I am getting data back however I am missing something.
import Foundation
struct RatingsResources: Codable {
let golfcourserating : [GolfCourseRating]?
}
struct GolfCourseRating: Codable {
let id: UUID = UUID()
let courseID: Int?
let teeColor: String?
let teeboxtype: String?
let teeslope: Double?
let teerating: Double?
enum CodingKeysRatings: String, CodingKey {
case courseID = "courseid"
case teeColor = "color"
case teeboxtype
case teeslope = "slope"
case teerating = "rating"
}
}
func getCoureRating(courseID: String?) {
let semaphore = DispatchSemaphore (value: 0)
print("GETTING COURSE TEE RATINGS..........")
let urlString: String = "https://api.golfbert.com/v1/courses/\(courseID ?? "4800")/teeboxes"
print ("API STRING: \(urlString) ")
let url = URLComponents(string: urlString)!
let request = URLRequest(url: url.url!).signed
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let decoder = JSONDecoder()
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
if let response = try? JSONDecoder().decode([RatingsResources].self, from: data) {
DispatchQueue.main.async {
self.ratingresources = response
}
return
}
print("*******Data String***********")
print(String(data: data, encoding: .utf8)!)
print("***************************")
let ratingsData: RatingsResources = try! decoder.decode(RatingsResources.self, from: data)
print("Resources count \(ratingsData.golfcourserating?.count)")
semaphore.signal()
}
task.resume()
semaphore.wait()
} //: END OF GET COURSE SCORECARD
First of all, never use try? while decoding your JSON. This will hide all errors from you. Use try and an appropriate do/catch block. In the catch block at least print the error.
Looking at your model there seem to be three issues here.
You don´t have an array of RatingsResources in your array. It is just a single instance.
let response = try JSONDecoder().decode(RatingsResources.self, from: data)
RatingsResources is not implemented correct.
let golfcourserating : [GolfCourseRating]?
should be:
let resources: [GolfCourseRating]?
Your coding keys are implemented wrong instead of:
enum CodingKeysRatings: String, CodingKey {
it should read:
enum CodingKeys: String, CodingKey {
You should add enum CodingKey with resources at struct RatingsResources
And decode:
if let response = try? JSONDecoder().decode(RatingsResources.self, from: data) {
// Your response handler
}
I am working with an api and getting back some strangely formatted JSON.
[
{
"title": "Wales' new £2bn space strategy hopes",
"url": "https://www.bbc.co.uk/news/uk-wales-60433763",
"source": "bbc"
},
{
"title": "Could Port Talbot become a centre of space tech? Video, 00:02:02Could Port Talbot become a centre of space tech?",
"url": "https://www.bbc.co.uk/news/uk-wales-60471170",
"source": "bbc"
},
]
As you can see, there is no object name I can latch on to. I've tried making a model like this
struct SpaceNewsModel: Identifiable, Codable {
var id = UUID()
let title: String
let url: String
let source: String
}
But once I get to using JSONDeocder() with the following code
let decoder = JSONDecoder()
if let safeData = data {
do {
let astroNews = try decoder.decode(SpaceNewsModel.self, from: safeData)
print(astroNews)
} catch {
print("DEBUG: Error getting news articles \(error.localizedDescription)")
}
}
I get the error DEBUG: Error getting news articles The data couldn’t be read because it isn’t in the correct format.
So how would you go about printing out each title, url, or source to the console? I've worked with JSON before and they are usually formatted differently.
I found a workaround for this. Removing the identifiable protocol lets me access the data. Like so
struct SpaceNewsModel: Codable {
let title: String
let url: String
let source: String
}
Then I can use decoder as normal
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
if error != nil {
print(error!)
} else {
let decoder = JSONDecoder()
if let safeData = data {
do {
let astroNews = try decoder.decode([SpaceNewsModel].self, from: safeData)
print(astroNews[0].title)
} catch {
print("DEBUG: Error getting news articles \(error)")
}
}
I did change let astroNews = try decoder.decode(SpaceNewsModel.self, from: safeData) to let astroNews = try decoder.decode([SpaceNewsModel].self, from: safeData)
Even with changing it to array type or not, as long as I had my SpaceNewsModel following the identifiable protocol, it would NOT work. It's a strange workaround, but it works for now.
in addition to
let astroNews = try decoder.decode([SpaceNewsModel].self, from: safeData)
use this common approach for your SpaceNewsModel. Having it Identifiable is very useful in SwiftUI.
struct SpaceNewsModel: Identifiable, Codable {
let id = UUID() // <-- here, a let
let title: String
let url: String
let source: String
enum CodingKeys: String, CodingKey { // <-- here
case title,url,source
}
}
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
}
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 am attempting to parse JSON with codable in Swift. I have successfully done this before, but I have a more complicated JSON object with some arrays and I am having trouble.
Here is my JSON:
{
"data": [ {
"type":"player",
"id":"account.7e5b92e6612440349afcc06b7c390114",
"attributes": {
"createdAt":"2018-04-06T04:59:40Z",
"name":"bob",
"patchVersion":"",
"shardId":"pc-na",
"stats":null,
"titleId":"bluehole-pubg",
"updatedAt":"2018-04-06T04:59:40Z"
},
"relationships": {
"assets": {
"data":[]
},
"matches": {
"data": [
{"type":"match","id":"3e2a197a-1453-4569-b35b-99e337dfabc5"},
{"type":"match","id":"15f41d2f-9da2-4b95-95ca-b85e297e14b7"},
{"type":"match","id":"a42c496c-ad92-4d3e-af1f-8eaa2e200c2b"}
{"type":"match","id":"b6e33df5-4754-49da-9a0f-144842bfc306"},
{"type":"match","id":"5b357cd1-35fe-4859-a2d7-48f263120bbd"},
{"type":"match","id":"99fc5f81-c24c-4c82-ae03-cd21c94469c0"},
{"type":"match","id":"1851c88e-6fed-48e8-be84-769f20f5ee6f"},
{"type":"match","id":"e16db7ea-520f-4db0-b45d-649264ac019c"},
{"type":"match","id":"6e61a7e7-dcf5-4df5-aa88-89eca8d12507"},
{"type":"match","id":"dcbf8863-9f7c-4fc9-b87d-93fe86babbc6"},
{"type":"match","id":"0ba20fbb-1eaf-4186-bad5-5e8382558564"},
{"type":"match","id":"8b104f3b-66d5-4d0a-9992-fe053ab4a6ca"},
{"type":"match","id":"79822ea7-f204-47f8-ae6a-7efaac7e9c90"},
{"type":"match","id":"1389913c-a742-434a-80c5-1373e115e3b6"}
]
}
},
"links": {
"schema":"",
"self":"https://api.playbattlegrounds.com/shards/pc-na/players/account.7e5b92e6612440349afcc06b7c390114"
}
}],
"links": {
"self":"https://api.playbattlegrounds.com/shards/pc-na/players?filter[playerNames]=dchilds64"
},
"meta":{}
}
Here are the models I am using:
public struct PlayerResponse: Codable {
let data: [Player]
}
For Player:
public struct Player: Codable {
let type: String
let id: String
let attributes: Attributes
let relationships: Relationships
}
For Attributes:
public struct Attributes: Codable {
let name: String
let patchVersion: String
let shardId: String
let titleId: String
let updatedAt: String
}
For Relationships:
public struct Relationships: Codable {
let matches: Matches
}
For Matches:
public struct Matches: Codable {
let data: [Match]
}
For Match:
public struct Match: Codable {
let type: String
let id: String
}
Decoding as:
let players = try decoder.decode([Player].self, from: jsonData)
I have this function which runs my network request:
func getPlayerData(for name: String, completion: ((Result<[Player]>) -> Void)?) {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "api.playbattlegrounds.com"
urlComponents.path = "/shards/\(regionShard.rawValue)/players"
let playerNameItem = URLQueryItem(name: "filter[playerNames]", value: "\(name)")
urlComponents.queryItems = [playerNameItem]
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
print(url)
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/vnd.api+json", forHTTPHeaderField: "Accept")
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonData = responseData {
let decoder = JSONDecoder()
do {
let players = try decoder.decode([Player].self, from: jsonData)
completion?(.success(players))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
The problem I am facing is that I get this error when I try to run the network request:
I think there is an issue with my codable structs, but I'm not sure. Could someone point me in the right direction to look for my error?
I suggest you build this up from the ground, since the errors of JSONDecoder (as with any compiler) get worse the more involved your structures are. Let's see how far we get:
Your Match struct is pretty sound:
public struct Match: Codable {
let type: String
let id: String
}
let decoder = JSONDecoder()
let mData = """
{"type":"match","id":"3e2a197a-1453-4569-b35b-99e337dfabc5"}
""".data(using:.utf8)!
let match = try! decoder.decode(Match.self, from:mData)
print(match)
No unexpected problems here. Shortening Matches a bit you already get your first error, rather an unexpected one
public struct Matches: Codable {
let data: [Match]
}
let mtchsData = """
{
"data": [
{"type":"match","id":"3e2a197a-1453-4569-b35b-99e337dfabc5"},
{"type":"match","id":"15f41d2f-9da2-4b95-95ca-b85e297e14b7"},
{"type":"match","id":"a42c496c-ad92-4d3e-af1f-8eaa2e200c2b"}
{"type":"match","id":"b6e33df5-4754-49da-9a0f-144842bfc306"},
{"type":"match","id":"5b357cd1-35fe-4859-a2d7-48f263120bbd"}
]
}
""".data(using:.utf8)!
do {
let mtches = try decoder.decode(Matches.self, from:mtchsData)
print(mtches)
} catch {
print(error)
}
will print the following error:
"dataCorrupted(Swift.DecodingError.Context(codingPath: [],
debugDescription: "The given data was not valid JSON.",
underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840
"Badly formed array around character 233."
UserInfo={NSDebugDescription=Badly
formed array around character 233.})))\n"
This is a trivial error, you are missing a comma on line 3 of the data Array. Adding that all will go well, but if it comes like this from your service you will have to fix it first.
I guess you get the idea and know your way about building up the structure successively. On the top level you will notice that your top level structure goes beyond an array of Player, it is actually a Dictionary with "data" as its sole key as you modelled correctly in PlayerResponse as #AnkitJayaswal pointed out already. That makes two errors already, those are the ones I managed to spot easily, but as I suggested before you should continue the build up of tests, that way you will know that the "lower" levels parse correctly and can concentrate on the problem at hand.
All of the above works easily in a Playground and there is no need to actually call the WebService in the process. Of course you will have to import Cocoa, but you already knew that. Anyway it always helps to reduce the level of complexity by splitting up your problem into smaller parts.
As I can see your whole player response is in key data. And your parsing player info with Player codable struct directly rather than data key which is used in PlayerResponse codable struct.
To resolve this update your code as:
let players = try decoder.decode(PlayerResponse.self, from: jsonData)
Hope this will solve your problem.