Parsing JSON With Codable in Swift - json

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.

Related

Swift Data Model from JSON Response

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
}

Nested Json data won't be decoded using Swift language?

I'm receiving response data from an API, when I'm trying to decode it by using json decoder, the nested json data won't be decoded because it returns null.
json data as follow:
{
"token": "string",
"details": {
"ID": "string",
"Name": "string",
"Message": null
}
}
Decoding model is:
struct response: Codable {
let token: String?
let usrData: userData?
}
struct userData:Codable{
let ID,Name,Message: String?
}
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
completion(.failure(.custom(errorMessage: "Please check internet connection")))
return
}
guard let loginResponse = try? JSONDecoder().decode(response.self, from:data) else
{
completion(.failure(.invalidCredentials))
return
}
print(loginResponse.userData?.userID as Any) //returns nil
print(loginResponse.token) //token printed
guard let token = loginResponse.token else {
completion(.failure(.invalidCredentials))
return
}
completion(.success(token))
}.resume()
The token from the response will be successfully decoded, but the userData returns null.
Your model's property name and decoded json property name must be equal if your are not mapping , so change your struct with :
struct response: Codable {
let token: String?
let details: userDto?
}
That certainly can not work, first, in your response struct you have a let variable that says usrData that can not be identified as details. Second you write usrData: userDto what is userDto you clearly did a mistake or forgot to mention it. However, do it like that for example:
struct Response: Codable {
let token: String?
let details: UserData?
}
struct UserData: Codable {
let ID,Name,Message: String?
}
let filePath = Bundle.main.path(forResource:"test", ofType: "json")
let data = try Data(contentsOf: URL(fileURLWithPath: filePath!))
if let loginResponse = try? JSONDecoder().decode(Response.self, from: data) {
loginResponse
}
The example is not completely correct, because fileURLWithPath is deprecated but you should get the idea from it.
I also recommend following some basic roles, like writing Structs with an uppercase letter.

How would I print the title property in this returned JSON in Swift using URLSession and dataTask?

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
}
}

How do I parse the nested JSON data from Brawl Stars api in Swift?

The API website I used. I made an account -> Documentation -> Brawlers if you want to try it out
Here is part of the JSON I want to parse. I want to print the name inside the "starPowers" array as well as the "gadgets" array.
{
"items":[
{
"id":16000014,
"name":"BO",
"starPowers":[
{
"id":23000090,
"name":"CIRCLING EAGLE"
},
{
"id":23000148,
"name":"SNARE A BEAR"
}
],
"gadgets":[
{
"id":23000263,
"name":"SUPER TOTEM"
},
{
"id":23000289,
"name":"TRIPWIRE"
}
]
}
]
}
I tried this first way of parsing the JSON which worked but I couldn't find a way to print the "name" inside the "starPower" or "gadgets" array.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let apiToken = "abcdefg"
if let url = URL(string: "https://api.brawlstars.com/v1/brawlers") {
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("Bearer \(apiToken)", forHTTPHeaderField: "authorization")
request.addValue("application/json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
} else {
guard let data = data else {return}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as AnyObject
print(jsonResult)
if let items = jsonResult["items"] as? NSArray {
for item in items as [AnyObject] {
if let name = item["name"] {
print(name!)
}
if let gadgets = item["gadgets"] {
print(gadgets!)
}
if let starPowers = item["starPowers"]{
print(starPowers!)
}
}
}
} catch {
print("JSON processing failed: \(error.localizedDescription)")
}
}
}.resume()
} else {
print("Something went wrong")
}
}
}
So I added another file with this data:
struct StarPowers: Codable {
let starPowerName: String
}
struct Gadgets: Codable {
let gadgetName: String
}
struct Items: Codable {
let name: String
let starPowers: [StarPowers]
let gadgets: [Gadgets]
}
And replaced the code inside the do statement with this but it returned "JSON processing failed: The data couldn’t be read because it is missing." (Catch statement)
let items = try JSONDecoder().decode(Items.self, from: data)
print(items.name.first ?? "")
I'm still fairly new to Swift in general as well as StackOverflow, so any help or feedback will be greatly appreciated. Thanks!
When using the Codable APIs you want to model all the data you see. So this JSON:
{
"items":[
{
"id":16000014,
"name":"BO",
"starPowers":[
{
"id":23000090,
"name":"CIRCLING EAGLE"
},
{
"id":23000148,
"name":"SNARE A BEAR"
}
],
"gadgets":[
{
"id":23000263,
"name":"SUPER TOTEM"
},
{
"id":23000289,
"name":"TRIPWIRE"
}
]
}
]
}
should result in these structs:
//This represents one star power
struct StarPower: Codable {
let id: Int
let name: String
}
//This represents one gadget
struct Gadget: Codable {
let id: Int
let name: String
}
//This represents one "item" (I think they're brawlers, but I didn't make an account so I can't confirm what the API calls them
struct Item: Codable {
let id: Int
let name: String
let starPowers: [StarPower]
let gadgets: [Gadget]
}
However, the excerpt that you provided is actually of type [String:[Item]] (a dictionary (aka a JSON object) with one string key, with the value being an array of items. You could make a Codable struct to handle that, or you can just do this:
let decoded = try! JSONDecoder().decode([String:[Item]].self, from: data)
let items = decoded["items"]!
//items is an array of type [Item]
// Using your example, it would only have one element.
let element = items.first!
for starPower in element.starPowers {
print(starPower.name)
}
for gadget in element.gadgets {
print(gadget.name)
}
(keep in mind that I'm force-unwrapping and force-trying because I'm assuming that fetching the data worked fine and that it is correct. You should probably check these assumptions and use constructs such as if-let and do-catch)
Name the properties of your codable structs exactly as the keys in the corresponding JSON:
struct StarPower: Codable {
let name: String
}
struct Gadget: Codable {
let name: String
}
struct Item: Codable {
let name: String
let starPowers: [StarPower]
let gadgets: [Gadget]
}
The decoder couldn't find the data, because "gadgetName" and "starPowerName" are not part of the JSON you provided.
Another tip: use singular while naming structs, so one element of gadgets is a gadget. the decoder doesn't care how you name your structs, it only cares about the properties

Accessing JSON Array from API response in Swift

I am brand new to writing swift so any tips on improvement/best practices are welcome but my main issue is that I am having trouble accessing a nested JSON array list. I am using this free API and trying to show a list of characters https://swapi.dev/api/people/
Please see the code snippet below.
When I print the type its : Optional<Any> and when i print json["results"] it prints the array like:
Optional(<__NSArrayI 0x600000fe31e0>(
{
"birth_year" = 19BBY;
created = "2014-12-09T13:50:51.644000Z";
....
I have tried several different things but have been unsuccessful. Could someone please give some advice on how I might iterate the list under json["results"?
func onLoad() -> Void {
let url = URL(string: "https://swapi.dev/api/people")
guard let requestUrl = url else { fatalError() }
// Create URL Request
var request = URLRequest(url: requestUrl)
// Specify HTTP Method to use
request.httpMethod = "GET"
// Send HTTP Request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Check if Error took place
if let error = error {
print("Error took place \(error)")
return
}
// Convert HTTP Response Data to a simple String
if let data = data {
// let json = try? JSONSerialization.jsonObject(with: data, options: [])
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
// try to read out a string array
print(type(of: json["results"]))
print(json["results"])
}
} catch let error as Error {
print("Failed to load: \(error.localizedDescription)")
}
}
}
task.resume()
}
Thanks for any help!
You should really be using Decodable rather than trying to parse it with JSON as that can easily lead to errors as you are accessing values by strings and it doesn't allow the IDE to help you.
You need to create some objects that describe what you are getting in your response.
Your main json response is made up of the following
{
"count": 82,
"next": "http://swapi.dev/api/people/?page=2",
"previous": null,
"results": [...]
}
This allows you to create a People struct that conforms to Decodable.
struct People: Decodable {
let count: Int
let next: URL?
let previous: URL?
let results: [Person]
}
The results array is really what you are after as that contains all the information about a person.
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "http://swapi.dev/api/planets/1/",
"films": [
"http://swapi.dev/api/films/1/",
"http://swapi.dev/api/films/2/",
"http://swapi.dev/api/films/3/",
"http://swapi.dev/api/films/6/"
],
"species": [],
"vehicles": [
"http://swapi.dev/api/vehicles/14/",
"http://swapi.dev/api/vehicles/30/"
],
"starships": [
"http://swapi.dev/api/starships/12/",
"http://swapi.dev/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "http://swapi.dev/api/people/1/"
}
We can represent this with the following struct called Person that also conforms to Decodable
struct Person: Decodable {
let name: String
let height: String
let mass: String
let hairColor: String
let skinColor: String
let birthYear: String
let gender: Gender
let homeworld: String
let films: [URL]
let species: [URL]
let vehicles: [URL]
let starships: [URL]
let created: Date
let edited: Date
let url: URL
}
enum Gender: String, Decodable {
case male
case female
case unknown = "n/a"
}
Note a couple of differences between the names in the struct and the names in the object that you are getting back. eg hair_color (snakecase) and hairColor (camelCase) In Swift it is common to write it the latter way and when we use decodable we can tell our decoder to use a custom key decoding strategy. Also note that I have used an enum for Gender. This isn't required and we could have just used a String. Also note that created and edited are Dates, however they are not iso8601 compliant but we can also specify a custom date decoding strategy.
Here is how we can decode the data that you have received.
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let people = try decoder.decode(People.self, from: data)
Now we can put this all together in your network request to get the following:
func onLoad() {
let url = URL(string: "https://swapi.dev/api/people")
guard let requestUrl = url else { fatalError() }
// Create URL Request
var request = URLRequest(url: requestUrl)
// Specify HTTP Method to use
request.httpMethod = "GET"
// Send HTTP Request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Check if Error took place
if let error = error {
print("Error took place \(error)")
return
}
// Convert HTTP Response Data to a simple String
if let data = data {
do {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let people = try decoder.decode(People.self, from: data)
people.results.forEach { person in print(person) }
} catch {
print("Failed to load: \(error)")
}
}
}
task.resume()
}
Cast results as an Array of Dictionary. Here's how
if let data = data {
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let results = json["results"] as? [[String: Any]] {
for result in results {
print(result)
}
}
} catch {
print("Failed to load: \(error.localizedDescription)")
}
}
Better Approach: Use Codable, JSONSerialization feels bit outdated.
Related Links:
https://developer.apple.com/documentation/swift/codable
https://www.swiftbysundell.com/basics/codable/