Parsing Json from Web API - json

Hello my name is Nico,
I am a complete beginner in App programming/SwiftUI.
I am trying to parse json data from an web api but somehow I cannot parse the data correctly. I assume that my json structure is not correct but I cannot find the problem.
The Json which I get from the Web API looks something like this:
{
"pois": [
{
"id": "2635094451",
"lat": "52.410150",
"lat_s": "52.4",
"lng": "10.776630",
"lng_s": "10.8",
"street": "Röntgenstraße",
"content": "8137285512",
"backend": "0-239283152",
"type": "1",
"vmax": "50",
"counter": "0",
"create_date": "2021-11-18 13:21:50",
"confirm_date": "2021-11-18 13:21:43",
"gps_status": "-",
"info": " {\"qltyCountryRoad\":1,\"confirmed\":\"0\",\"gesperrt\":\"0\",\"precheck\":\"[Q1|21|0]\"}",
"polyline": ""
}
],
"grid": []
}
My Structure looks like this:
struct APIResponse: Codable {
let pois: [InputDataPois]
let grid: [InputDataGrid]
private enum CodingKeys: String, CodingKey {
case pois
case grid
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.pois = try container.decode(APIResponse.self, forKey: .pois).pois
self.grid = try container.decode(APIResponse.self, forKey: .grid).grid
}
}
struct InputDataPois: Codable, Identifiable {
let id:String
let lat:String
let lat_s:String
let lng:String
let lng_s:String
let street:String
let content:String
let backend:String
let type:String
let vmax:String
let counter:String
let create_date:String
let confirm_date:String
let gps_status:String
let info:String
let polyline:String
}
extension InputDataPois {
private enum CodingKeys: String, CodingKey {
case id
case lat
case lat_s
case lng
case lng_s
case street
case content
case backend
case type
case vmax
case counter
case create_date
case confirm_date
case gps_status
case info
case polyline
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.lat = try container.decode(String.self, forKey: .lat)
self.lat_s = try container.decode(String.self, forKey: .lat_s)
self.lng = try container.decode(String.self, forKey: .lng)
self.lng_s = try container.decode(String.self, forKey: .lng_s)
self.street = try container.decode(String.self, forKey: .street)
self.content = try container.decode(String.self, forKey: .content)
self.backend = try container.decode(String.self, forKey: .backend)
self.type = try container.decode(String.self, forKey: .type)
self.vmax = try container.decode(String.self, forKey: .vmax)
self.counter = try container.decode(String.self, forKey: .counter)
self.create_date = try container.decode(String.self, forKey: .create_date)
self.confirm_date = try container.decode(String.self, forKey: .confirm_date)
self.gps_status = try container.decode(String.self, forKey: .gps_status)
self.info = try container.decode(String.self, forKey: .info)
self.polyline = try container.decode(String.self, forKey: .polyline)
}
}
struct InputDataGrid: Codable {
}
and my Bundle like this:
extension Bundle {
func decode(_ file: String) -> [InputDataPois] {
// 1. Locate the Json File
guard let url = URL(string: file) else {
fatalError("Failed to locate \(file) in bundle")
}
// 2.Create a property for the Data
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to Load \(file) from bundle.")
}
// 3. Create a property for the data
let str = String(decoding: data, as: UTF8.self)
print("\(str)")
guard let loaded = try? JSONDecoder().decode(APIResponse.self, from: data).pois else {
fatalError("Failed to decode \(file) from bundle.")
}
// 4. Return the ready-to-use data
return loaded
}
}
And In my View I am using:
let speed: [InputDataPois] = Bundle.main.decode("https://cdn2.atudo.net/api/1.0/vl.php?type=0,1,2,3,4,5,6&box=52.36176390234046,10.588760375976562,52.466468685912744,11.159706115722656")
The error I am getting looks something like this.
ErrorMessage
ConsoleError
Thanks in advance for you help.

Remove your custom Codable implementation and coding keys. They are not necessary, the compiler can generate them for you with this simple JSON. Then everything should work.
The problem here specifically is your APIResponse decoding:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.pois = try container.decode(APIResponse.self, forKey: .pois).pois
self.grid = try container.decode(APIResponse.self, forKey: .grid).grid
}
To decode an APIResponse you try to decode two APIResponses. This again would try to decode APIResponse leading to endless recursion. The only reason you are not seeing that is that your JSON contains arrays making the second call to container(keyedBy:) fail. Try setting a breakpoint (or if you must a print statement) on the first line of your init(from:) and you will see that this is called a second time before it fails.
To fix that you would need to decode the actual type of your properties:
self.pois = try container.decode([InputDataPois].self, forKey: .pois)
But as I said, there is nothing in your JSON format requiring manually implementing the decoder, so the best is to let the compiler synthesize it for you.

Related

Parsing a JSON object with dynamic content: what is the best way to do it with low cyclomatic complexity?

I have a service that returns an array of objects in this form:
{
result: [
{
objectId: "id",
type: "objectType",
content: { ... }
}, ...
]
}
The content depends on the object type. I've tried building up a custom decoder this way (ContentItem is a protocol for ClassA, B and so on):
struct ContentDataItem: Decodable {
var objectId: String?
var type: String?
var content: ContentItem?
private enum CodingKeys: String, CodingKey {
case objectId
case type
case content
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
objectId = try container.decode(String.self, forKey: .objectId)
type = try container.decode(String.self, forKey: .type)
if let type = type {
switch type {
case "typeA":
content = try container.decode(ClassA.self, forKey: .content)
case "typeB":
content = try container.decode(ClassB.self, forKey: .content)
...
}
}
}
}
This works, but I get a high cyclomatic complexity (I'm aiming for sub-10, but I have 14 different classes for content). So I've tried changing my approach, making ContentItem the superclass and changing the init into something like this:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
objectId = try container.decode(String.self, forKey: .objectId)
let type = try container.decode(String.self, forKey: .type)
let itemTypes: [String: ContentItem.Type] = [
"typeA": ClassA.self,
"typeB": ClassB.self,
...
]
guard let type = type, let contentItemType = itemTypes[type] else { return }
content = try container.decode(contentItemType, forKey: .content)
}
This reduces the cyclomatic complexity as I wanted, but it doesn't work anymore because the decode only returns objects of type ContentItem (the superclass), not the specific ClassA, ClassB that I want. Is there a way to make this approach work? What is wrong with it?
And more importantly, is there a more efficient way to parse this object?

Swift: JSON Decode fails in some cases

I have this struct
struct ProductInfo: Codable {
var code: String
var product_name: String?
var status: Int
enum CodingKeys: String, CodingKey {
case code
case product
case status
enum ProductCodingKeys: String, CodingKey {
case product_name
}
}
init(from decoder: Decoder) throws {
let rootContainer = try! decoder.container(keyedBy: CodingKeys.self)
let productContainer = try! rootContainer.nestedContainer(keyedBy: CodingKeys.ProductCodingKeys.self, forKey: .product)
product_name = try! productContainer.decode(String.self, forKey: .product_name)
code = try! rootContainer.decode(String.self, forKey: .code)
status = try! rootContainer.decode(Int.self, forKey: .status)
}
func encode(to encoder: Encoder) throws {
var rootContainer = encoder.container(keyedBy: CodingKeys.self)
var productContainer = rootContainer.nestedContainer(keyedBy: CodingKeys.ProductCodingKeys.self, forKey: .product)
try rootContainer.encode(code, forKey: .code)
try rootContainer.encode(status, forKey: .status)
try productContainer.encode(product_name, forKey: .product_name)
}
}
Problem is that is the product is not found on the DB, the CodingKey "product" will not exist, only code and status are available.
That means, this line
let productContainer = try! rootContainer.nestedContainer(keyedBy: CodingKeys.ProductCodingKeys.self, forKey: .product)
Will fail because the key is not found in the JSON Response.
My idea was to check the status value (which I have in the Response) but it seems not working
Thanks
You can use contains(_:):
if rootContainer.contains(.product) {
let productContainer = try rootContainer.nestedContainer(keyedBy: CodingKeys.ProductCodingKeys.self, forKey: .product)
product_name = try productContainer.decode(String.self, forKey: .product_name)
}
To test it:
let jsonStr = """
[
{
"code": "8001620004159",
"product": { "_id": "8001620004159", "product_name": "The San Benedetto" },
"status": 1,
"status_verbose": "product found"
},
{
"code": "4007843212122",
"status": 0,
"status_verbose": "product not found"
}
]
"""
let jsonData = Data(jsonStr.utf8)
do {
let decoder = JSONDecoder()
let items = try decoder.decode([ProductInfo].self, from: jsonData)
print(items)
} catch {
print("Error: \(error)")
}
Side note:
I'd avoid try! and replace them with try since the method throws already.

Expected to decode Dictionary<String, Any> but found an array instead with Nested Containers

So, I am trying to parse this JSON using the Codable protocols:
https://randomuser.me/api/?results=100
That are basically 100 random users.
Here's my User class initializer from decoder, that I need because the User is an entity in a Core Data Model:
required convenience public init(from decoder: Decoder) throws {
let managedObjectContext = CoreDataStack.sharedInstance.persistentContainer.viewContext
guard let entity = NSEntityDescription.entity(forEntityName: "User", in: managedObjectContext) else {
fatalError("Failed to decode User")
}
self.init(entity: entity, insertInto: managedObjectContext)
let container = try decoder.container(keyedBy: CodingKeys.self)
let results = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .results)
let name = try results.nestedContainer(keyedBy: CodingKeys.self, forKey: .name)
firstName = try name.decode(String.self, forKey: .firstName)
lastName = try name.decode(String.self, forKey: .lastName)
let location = try results.nestedContainer(keyedBy: CodingKeys.self, forKey: .location)
let street = try location.decode(String.self, forKey: .street)
let city = try location.decode(String.self, forKey: .city)
let postcode = try location.decode(String.self, forKey: .postcode)
address = street + ", " + city + ", " + postcode
email = try results.decode(String.self, forKey: .email)
let pictures = try results.nestedContainer(keyedBy: CodingKeys.self, forKey: .pictures)
pictureURL = try pictures.decode(String.self, forKey: .pictureURL)
}
This is the defective line:
let results = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .results)
Here's the complete error:
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
I think it's due to the structure of the JSON, that is an array of 100 elements under the key "results", and I think the problem could be in doing them all together.
How should I handle this?
The error is clear: The value for results is an array and nestedContainers expects a dictionary.
To decode the user array you need an umbrella struct outside of the Core Data classes.
struct Root : Decodable {
let results: [User]
}
While decoding Root the init method in User is called for each array item.
To use nestedContainers you have to separate the CodingKeys.
This is the init method without the Core Data stuff. postcode can be String or Int
private enum CodingKeys: String, CodingKey { case name, location, email, picture }
private enum NameCodingKeys: String, CodingKey { case first, last }
private enum LocationCodingKeys: String, CodingKey { case street, city, postcode }
private enum PictureCodingKeys: String, CodingKey { case large, medium, thumbnail }
required convenience public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let nameContainer = try container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)
let firstName = try nameContainer.decode(String.self, forKey: .first)
let lastName = try nameContainer.decode(String.self, forKey: .last)
let locationContainer = try container.nestedContainer(keyedBy: LocationCodingKeys.self, forKey: .location)
let street = try locationContainer.decode(String.self, forKey: .street)
let city = try locationContainer.decode(String.self, forKey: .city)
let postcode : String
do {
let postcodeInt = try locationContainer.decode(Int.self, forKey: .postcode)
postcode = String(postcodeInt)
} catch DecodingError.typeMismatch {
postcode = try locationContainer.decode(String.self, forKey: .postcode)
}
let address = street + ", " + city + ", " + postcode
let email = try container.decode(String.self, forKey: .email)
let pictureContainer = try container.nestedContainer(keyedBy: PictureCodingKeys.self, forKey: .picture)
let pictureURL = try pictureContainer.decode(URL.self, forKey: .large)
}
This is a very simplified version but it handles your Json data correctly
struct Result : Codable {
let results: [User]
}
struct User: Codable {
let gender: String
let name: Name
}
struct Name: Codable {
let title: String
let first: String
let last: String
}
let decoder = JSONDecoder()
let data = jsonString.data(using: .utf8) //Replace with url download
do {
let json = try decoder.decode(Result.self, from: data!)
} catch {
print(error)
}

Decodable JSONDecoder handle different coding keys for the same value

I'm using Swift decodable protocol to parse my JSON response:
{
"ScanCode":"4122001131",
"Name":"PINK",
"attributes":{
"type":"Product",
"url":""
},
"ScanId":"0000000kfbdMA"
}
I'm running into an issue where sometimes I get the ScanId value with a key "Id" instead of "ScanId".
Is there a way to work around that?
Thanks
You have to write a custom initializer to handle the cases, for example
struct Thing : Decodable {
let scanCode, name, scanId : String
private enum CodingKeys: String, CodingKey { case scanCode = "ScanCode", name = "Name", ScanID, Id }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
scanCode = try container.decode(String.self, forKey: .scanCode)
name = try container.decode(String.self, forKey: .name)
if let id = try container.decodeIfPresent(String.self, forKey: .Id) {
scanId = id
} else {
scanId = try container.decode(String.self, forKey: .ScanID)
}
}
}
First try to decode one key, if it fails decode the other.
For convenience I skipped the attributes key

Swift4, JSON, keyNotFound, No value associated with key

I need to do Sunset Sunrise App, and this is my code. But I have this error:
Error serializing json: keyNotFound(Sunrise_Sunset.SunPosition.Results.(CodingKeys in _B4291256871B16D8D013EC8806040532).sunrise, Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key sunrise (\"sunrise\").", underlyingError: nil))
And I don't understand how to fix it. Maybe someone had this problem. I will be grateful for any help)
This is the API, which I have: https://sunrise-sunset.org/api
struct SunPosition: Codable {
struct Results: Codable {
let sunrise: String
let sunset: String
let solarNoon: String
let dayLenght: String
let civilTwilightBegin: String
let civilTwilightEnd: String
let nauticalTwilightBegin: String
let nauticalTwilightEnd: String
let astronomicalTwilightBegin: String
let astronomicalTwilightEnd: String
enum CodingKey:String, Swift.CodingKey {
case sunrise = "sunrise"
case sunset = "sunset"
case solarNoon = "solar_noon"
case dayLenght = "day_length"
case civilTwilightBegin = "civil_twilight_begin"
case civilTwilightEnd = "civil_twilight_end"
case nauticalTwilightBegin = "nautical_twilight_begin"
case nauticalTwilightEnd = "nautical_twilight_end"
case astronomicalTwilightBegin = "astronomical_twilight_begin"
case astronomicalTwilightEnd = "astronomical_twilight_end"
}
}
}
extension SunPosition.Results {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
sunrise = try values.decode(String.self, forKey: .sunrise)
sunset = try values.decode(String.self, forKey: .sunset)
solarNoon = try values.decode(String.self, forKey: .solarNoon)
dayLenght = try values.decode(String.self, forKey: .dayLenght)
civilTwilightBegin = try values.decode(String.self, forKey: .civilTwilightBegin)
civilTwilightEnd = try values.decode(String.self, forKey: .civilTwilightEnd)
nauticalTwilightBegin = try values.decode(String.self, forKey: .nauticalTwilightBegin)
nauticalTwilightEnd = try values.decode(String.self, forKey: .nauticalTwilightEnd)
astronomicalTwilightBegin = try values.decode(String.self, forKey: .astronomicalTwilightBegin)
astronomicalTwilightEnd = try values.decode(String.self, forKey: .astronomicalTwilightEnd)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "https://api.sunrise-sunset.org/json?lat=36.7201600&lng=-4.4203400"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let sunPosition = try JSONDecoder().decode(SunPosition.Results.self, from: data)
print(sunPosition)
}catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
}
Four issues:
Add let results : Results in the SunPosition struct.
Typo: private enum CodingKeys: String, CodingKey rather than enum CodingKey :String, Swift.CodingKey, note the singular / plural difference, the private attribute is recommended but does not cause the issue.
Wrong type to decode: JSONDecoder().decode(SunPosition.self, from: data) rather than JSONDecoder().decode(SunPosition.Results.self, from: data).
To get the results you have to print(sunPosition.results).
Three notes:
Delete the entire extension. In this case you get the initializer for free.
Add &formatted=0 to the URL and set the dateDecodingStrategy of the decoder to .iso8601 to get Date objects. Change the type of all date related properties from String to Date and the type of dayLenght from String to TimeInterval. To change dateDecodingStrategy write
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let sunPosition = try decoder.decode(SunPosition.self ...
I recommend to handle the status, too. Add this in the SunPosition struct
let status : String
var success : Bool { return status == "OK" }
To rule it out, if for some reason your endpoint is returning an error message or nil data, you'll get this error.