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.
Related
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
}
}
I'm pretty new to developing apps and working with Xcode and Swift and trying to get a handle on how to decode JSON responses from an API. I'm also trying to follow MVVM practices.
I'm calling to an API that returns data structured like this:
"location": {
"latitude": -27.4748,
"longitude": 153.017 },
"date": "2020-12-21",
"current_time": "21:55:42.198",
"sunrise": "04:49",
"sunset": "18:42",
"...": "..."
}
My understanding is I need a struct to decode this information into. This is what my struct looks like (it may be incorrect):
struct Example: Decodable {
let location: Coordinates
let date: String
let current_time: String
let sunset: String
let sunset: String
let ... :String
struct Coordinates: Decodable{
let latitude: Double
let longitude: Double
}
}
So I want to be able to call this api. I have the correct address to call it because the dashboard on the website I'm using is showing I've hit it. Could I get some guidance on how to call it and decode the response? Currently I'm doing something like this:
if let url = URL(string: "this is the api web address"){
URLSession.shared.dataTask(with: url){ (with: data, options [] ) as?
[String:String]{
print("(\json)" + "for debugging purposes")
}}
else{
print("error")
}
.resume()
Thanks for any and all help. Again, my goal is to call this function, hit the API, decode the JSON response and store it into a variable I can use in my views elsewhere. Thanks!
You are mixing JSONSerialization with Codable and URLSession. Forget about JSONSerialization. Try to focus on Codable protocol.
struct Example: Decodable {
let location: Coordinates
let date: String
let currentTime: String
let sunrise: String
let sunset: String
}
struct Coordinates: Decodable {
let latitude: Double
let longitude: Double
}
This is how you can decode your json data synchronously
extension JSONDecoder {
static let shared: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
}
extension Data {
func decodedObject<T: Decodable>() throws -> T {
try JSONDecoder.shared.decode(T.self, from: self)
}
}
Playground testing
let json = """
{
"location": {
"latitude": -27.4748,
"longitude": 153.017 },
"date": "2020-12-21",
"current_time": "21:55:42.198",
"sunrise": "04:49",
"sunset": "18:42",
}
"""
let data = Data(json.utf8)
do {
let example: Example = try data.decodedObject()
print(example)
} catch {
print(error)
}
And fetching your data asynchronously
extension URL {
func getResult<T: Decodable>(completion: #escaping (Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: self) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error!))
return
}
do {
completion(.success(try data.decodedObject()))
} catch {
completion(.failure(error))
}
}.resume()
}
}
let url = URL(string: "https://www.example.com/whatever")!
url.getResult { (result: Result<Example, Error>) in
switch result {
case let .success(example):
print(example)
case let .failure(error):
print(error)
}
}
If you need help with your SwiftUI project implementing MVVM feel free to open a new question.
Correct Method to decode a local JSON file in SwiftUI
do {
let path = Bundle.main.path(forResource: "filename", ofType: "json")
let url = URL(fileURLWithPath: path!)
let data = try Data(contentsOf: url)
let decodedresults = try JSONDecoder().decode(struct_name.self, from: data)
print(decodedresults)
} catch {
print(error)
}
I'm using Codable for the first time and want to output the json result of Google Places details as a label.
However, when I print it, the console says "The data could n’t be read because it isn’t in the correct format.”.
I can't solve it by myself, so please tell me how to write it correctly.
Thanks.
The result of json
{
"html_attributions": [],
"result": {
"formatted_phone_number": "XXXX-XXX-XXX",
"website": "https://www.xxxxx.com/xxxxxx/"
},
"status": "OK"
}
Detail.swift
import Foundation
struct Details : Codable {
var formatted_phone_number : String!
var website : String!
}
ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
fetchDetailData {(details) in
for detail in details{
print(detail.website)
}
}
}
func fetchDetailData(completionHandler: #escaping ([Details]) -> Void){
let url = URL(string: "https://maps.googleapis.com/maps/api/place/details/json?place_id=\(place_id)&fields=formatted_phone_number,website&key=\(apikey)")!
let task = URLSession.shared.dataTask(with: url){
(data,respose, error)in
guard let data = data else{ return }
do {
let detailsData = try JSONDecoder().decode([Details].self, from: data)
completionHandler(detailsData)
}
catch{
let error = error
print(error.localizedDescription)
}
}.resume()
}
One of the issues there is that result is a dictionary not an array. You need also to decode the root structure to extract the result from it. Note that you can also change the website type from String to URL:
struct Root: Codable {
let htmlAttributions: [String] // make sure to define the proper type in case the collection is not empty
let result: Result
let status: String
}
struct Result: Codable {
let formattedPhoneNumber: String
let website: URL
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Root.self, from: data).result
print(result)
} catch {
print(error)
}
This will print
Result(formattedPhoneNumber: "XXXX-XXX-XXX", website: https://www.xxxxx.com/xxxxxx/)
could you try this;
struct Place {
let result: Details?
}
struct Details: Codable {
let phoneNumber: String?
let website: String?
enum CodingKeys: String, CodingKey {
case website
case phoneNumber = "formatted_phone_number"
}
}
and parse Place.self
you will also need to change "#escaping ([Details])" to "#escaping (Place)"
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
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/