Decoding JSON when feed is inconsistent - Swift - ios

I don’t know how to handle a JSON feed in swift with the decodable protocol when the feed gives me two different results. If there is more than one ‘entry’, the json looks like this and the entry value is an array of objects
{
"feed": {
"publisher": "Penguin",
"country": "ca"
},
"entry": [
{
"author": "Margaret Atwood",
"nationality": "Canadian"
},
{
"author": "Dan Brown",
"nationality": "American"
}
]
}
However, if there is only a single entry, the json looks like this where entry is just a dictionary
{
"feed": {
"publisher": "Penguin",
"country": "ca"
},
"entry": {
"author": "Margaret Atwood",
"nationality": "Canadian"
}
}
to decode the first case, I would use the following structs
struct Book: Decodable {
let feed: Feed
let entry: [Entry]
}
// MARK: - Entry
struct Entry: Decodable {
let author, nationality: String
}
// MARK: - Feed
struct Feed: Decodable {
let publisher, country: String
}
And then use something like this to decode the data retrieved
let object = try JSONDecoder().decode(Book.self, from: data)
How do I handle the case when the entry is not an array of objects?

You could possibly override the decoder for Book. What you can do is to try to unwrap an [Entry]s and upon failing that, just try to unwrap a single Entry.
For example:
struct Book: Decodable {
let feed: Feed
let entry: [Entry]
init (from decoder :Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
value = try container.decode([Entry].self, forKey: .value)
} catch {
let newValue = try container.decode(Entry.self, forKey: .value)
value = [newValue]
}
}
}
Note: that's not a comprehensive sample on what you want to do but just a way you can accomplish what you want to do

Related

Swift Decodable Request object [duplicate]

This question already has answers here:
Swift Codable multiple types
(2 answers)
Closed 1 year ago.
I have a request object that points to an API from Stripe. The response from stripe looks like this
{
"object": "list",
"url": "/v1/refunds",
"has_more": false,
"data": [
{
"id": "re_3Jkggg2eZvKYlo2C0ArxjggM",
"object": "refund",
"amount": 100,
"balance_transaction": null,
"charge": "ch_3Jkggg2eZvKYlo2C0pK8hM73",
"created": 1634266948,
"currency": "usd",
"metadata": {},
"payment_intent": null,
"reason": null,
"receipt_number": null,
"source_transfer_reversal": null,
"status": "succeeded",
"transfer_reversal": null
},
{...},
{...}
]
}
I've created a decodable Request struct that looks like this:
struct Request: Decodable {
var object: String
var url: String
var has_more: Bool
var data: [Any]
}
The issue I am having is that the Request object can have data that contains an array of several different Objects. A refund object, a card object, and others. Because of this, I've added [Any] to the request struct but am getting this error:
Type 'Request' does not conform to protocol 'Decodable'
This seems to be because decodable can't use Any as a variable type. How can I get around this and use a universal Request object with dynamic object types?
You could utilize enum to solve your problem, I have done this in the past with great success.
The code example below can be copy/pasted into the Playground for further tinkering. This is a good way increase your understanding of the inner workings when decoding JSON to Decodable objects.
Hope this can nudge you in a direction that will work in your case.
//: A UIKit based Playground for presenting user interface
import PlaygroundSupport
import Foundation
let json = """
{
"object": "list",
"url": "/v1/refunds",
"has_more": false,
"data": [
{
"id": "some_id",
"object": "refund",
"amount": 100,
},
{
"id": "some_other_id",
"object": "card",
"cardNumber": "1337 1447 1337 1447"
}
]
}
"""
struct Request: Decodable {
let object: String
let url: String
let has_more: Bool
let data: [RequestData] // A list of a type with a dynamic data property to hold object specific information.
}
// Type for objects of type 'card'.
struct Card: Decodable {
let cardNumber: String
}
// Type for objects of type 'refund'.
struct Refund: Decodable {
let amount: Int
}
// A simple enum for every object possible in the 'data' array.
enum RefundObject: String, Decodable {
case card
case refund
}
// An enum with associated values, mirroring the cases from RefundObject.
enum RefundData: Decodable {
case card(Card)
case refund(Refund)
}
// The base data object in the 'data' array.
struct RequestData: Decodable {
let id: String // Common properties can live in the outer part of the data object.
let object: RefundObject
let innerObject: RefundData // An enum that contain any of the cases defined within the RefundData enum.
enum CodingKeys: String, CodingKey {
case id
case object
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.object = try container.decode(RefundObject.self, forKey: .object)
// Here we decode id (I assumed it was a common property), and object.
// The object will be used to determine what the inner part of the data object should be.
switch object {
case .card:
// Set innerObject to the .card case with an associated value of Card.
self.innerObject = .card(
try Card(from: decoder)
)
case .refund:
// Set innerObject to the .refund case with an associated value of Refund.
self.innerObject = .refund(
try Refund(from: decoder)
)
}
}
}
let decoder = JSONDecoder()
let requestData = try decoder.decode(Request.self, from: json.data(using: .utf8)!)
// Example usage of the innerObject:
for data in requestData.data {
switch data.innerObject {
case .card(let card):
print(card.cardNumber)
case .refund(let refund):
print(refund.amount)
}
}
https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html
https://developer.apple.com/documentation/foundation/jsondecoder
struct Request: Decodable {
var object: String
var url: String
var has_more: Bool
var data: [MyData]
}
then you have also make a decodable struct for that data array
struct MyData:Decodable {
var id: String
var object: String
var amount:Int
balance_transaction:String?
....
}

The data couldn’t be read because it isn’t in the correct format (swift)

I want to take weather informations with JSON but there is an error : The data couldn’t be read because it isn’t in the correct format.
Error 'It looks like your post is mostly code; please add some more details.' in stackoverflow. Although I briefly describe my problem, it still expects an explanation from me :/
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://api.openweathermap.org/data/2.5/weather?q=bursa,tr&appid=00f63a1cff271776651468c0204c422c"
getData(from: url)
}
private func getData (from url : String){
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data , response , error in
guard let data = data , error == nil else {
print ("birşeyler ters gitti")
return
}
var main : Response?
do {
main = try JSONDecoder().decode(Response.self , from: data)
} catch{
print ("ERROR IS HERE!!! \(error.localizedDescription)")
}
guard let json = main else {
return
}
print (json.weather)
})
task.resume()
}}
struct Response : Codable {
let weather : myResult
let status : String
}
struct myResult : Codable {
let main : String
let description : String
let icon : String
}
API Response is like that :
{"coord": { "lon": 139,"lat": 35},
"weather": [
{
"id": 800,
"main": "Clear",
"description": "clear sky",
"icon": "01n"
}
],
"base": "stations",
"main": {
"temp": 281.52,
"feels_like": 278.99,
"temp_min": 280.15,
"temp_max": 283.71,
"pressure": 1016,
"humidity": 93
},
"wind": {
"speed": 0.47,
"deg": 107.538
},
"clouds": {
"all": 2
},
"dt": 1560350192,
"sys": {
"type": 3,
"id": 2019346,
"message": 0.0065,
"country": "JP",
"sunrise": 1560281377,
"sunset": 1560333478
},
"timezone": 32400,
"id": 1851632,
"name": "Shuzenji",
"cod": 200
}
First, error.localizedDescription is meant to display an information for the user. It's not useful for debugging. If you replace it with error:
} catch {
print ("ERROR IS HERE!!! \(error)") // <- remove .localizedDescription
}
you will get more details:
ERROR IS HERE!!! typeMismatch(Swift.Dictionary<Swift.String, Any>,
Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue:
"weather", intValue: nil)], debugDescription: "Expected to decode
Dictionary<String, Any> but found an array instead.", underlyingError:
nil))
To solve this you need to declare weather as an array:
let weather: [myResult]
I'd also recommend replacing myResult with Weather (or at least capitalised MyResult) as it will be more readable:
struct Weather: Codable {
let main: String
let description: String
let icon: String
}
Also, in the JSON response you provided there is no status field so you may need to remove it from the Response class (or make it optional).
If you'd like to add more fields to the response, declare them according to your JSON structure. Eg. if you want to add humidity and temperature you can do:
struct Response: Codable {
...
let main: Main
}
struct Main: Codable {
let temp: Double
let humidity: Double
}
To have a more readable code you can use CodingKeys - then your variable names can be independent from JSON variables.
struct Main: Codable {
enum CodingKeys: String, CodingKey {
case temperature = "temp"
case humidity
}
let temperature: Double
let humidity: Double
}
Summing up, your Response may look like this:
struct Response: Codable {
let weather: [Weather]
let main: Main
// alternatively declare `status` optional
// let status: String?
}
struct Weather: Codable {
let main: String
let description: String
let icon: String
}
struct Main: Codable {
enum CodingKeys: String, CodingKey {
case temperature = "temp"
case humidity
}
let temperature: Double
let humidity: Double
}

How would I read JSON data in an array using SwiftyJSON?

I have just been introduced to using JSON data in my iOS app. I used a service to parse JSON data from a website, and I would like to decode that data to display to my user in a UITableView. My JSON data looks as follows:
{
"success": true,
"outputScenario": "Default",
"data": {
"collection": [
{
"teamName": "Gulf Coast Monarchs, FL",
"teamW": "10",
"teamL": "0",
"teamT": "0",
"teamPct": "1.000",
"teamGB": "-",
"teamGP": "10",
"teamRA": "10",
"teamDivision": "10-0-0"
},
{
"teamName": "Ohio Nationals, OH",
"teamW": "9",
"teamL": "1",
"teamT": "0",
"teamPct": ".900",
"teamGB": "1.0",
"teamGP": "10",
"teamRA": "20",
"teamDivision": "9-1-0"
}, {
"teamName": "Mount Kisco Chiefs, NY",
"teamW": "0",
"teamL": "8",
"teamT": "0",
"teamPct": ".000",
"teamGB": "8.0",
"teamGP": "8",
"teamRA": "108",
"teamDivision": "0-8-0"
}
{
"teamName": "Mount Kisco Chiefs, NY",
"teamW": "0",
"teamL": "8",
"teamT": "0",
"teamPct": ".000",
"teamGB": "8.0",
"teamGP": "8",
"teamRA": "108",
"teamDivision": "0-8-0"
}
]
},
Just keep in mind that I have cut out a significant amount of the data provided in the JSON so it is easily viewable.
I would like to decode this data using SwiftyJSON if possible so I can display it to my user in a UITableView. For now, the UITableView will display the team name in the UITableView.textLabel.text and the teamW and teamL in the UITableView.detailTextLabel.text. How would I decode this data using SwiftyJSON? I am struggling to figure out how this type of structure would be decoded. I would like to use the model that I have created:
struct Standing: Decodable {
var teamName: String
var teamW: Int
var teamL: Int
var teamT: Int
var teamPct: Int
teamGB: Int
teamGP: Int
teamRA: Int
teamDivision: String
}
Why do you want to use SwiftyJSON as you already adopted Decodable?
The types in your struct are widely wrong because all values are String. And you need two other structs for the parent objects.
struct Root: Decodable {
let success : Bool
let outputScenario : String
let data : TeamData
}
struct TeamData: Decodable {
let collection : [Standing]
}
struct Standing: Decodable {
let teamName, teamW, teamL, teamT: String
let teamPct, teamGB, teamGP, teamRA: String
let teamDivision: String
}
Decode the data, the Standing array is in the variable standings.
do {
let result = try JSONDecoder().decode(Root.self, from: data)
let standings = result.data.collection
} catch {
print(error)
}
If you're using Decodable, you don't need to use SwiftyJSON at all, everything's built into Swift itself.
Use this as your model struct:
struct Standing: Codable {
let success: Bool
let outputScenario: String
let data: DataClass
}
struct DataClass: Codable {
let collection: [Collection]
}
struct Collection: Codable {
let teamName, teamW, teamL, teamT: String
let teamPct, teamGB, teamGP, teamRA: String
let teamDivision: String
}
and parse it like so:
do {
let standing = try JSONDecoder().decode(Standing.self, from: data)
} catch {
print(error)
}

using decodable to capture array of nested objects in single line

Below is my json response and the struct which I need to costruct out of it.
condition: I would not like to create any other struct apart from Response,Media and would like to parse in single line as specified below.
{
"name": "xxxx",
"title": "xxxxxxx",
"assets": [
{
"items": [
{
"id": "eeee",
"desc": "rrrrrr"
}, {
"id": "eeee",
}, {
"desc": "rrrrrr"
}]
}]
}
struct Response {
let name : String
let title : string
let items : [Media]
private enum codingKeys : String, CodingKey {
case name = "name"
case title = "title"
case items = "assets.items"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: codingKeys.self)
name = try container.decode(String.self, forKey: .name)
title = try container.decode(TrayLayout.self, forKey: .title)
items = try container.decode([Media].self, forKey: .items)
}
}
I managed to find a solution for your problem.
Considering following is sample json
let jsonString = """
{
"name": "xxxx",
"title": "xxxxxxx",
"assets": [
{
"items": [
{
"id": "id11",
"desc": "desc11"
}, {
"id": "id12",
}, {
"desc": "desc13"
}]
},{
"items": [
{
"id": "id21",
"desc": "desc21"
}, {
"id": "id22",
}, {
"desc": "desc23"
}]
}]
}
"""
Your structures will look as below
struct Media: Codable {
let id,desc: String?
enum CodingKeys: String, CodingKey {
case id,desc
}
}
struct Response: Decodable {
let name,title: String?
let items: [Media]?
enum CodingKeys: String, CodingKey {
case name,title,items
case assets = "assets"
}
// Decoding
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
title = try container.decode(String.self, forKey: .title)
// Here as the assets contains array of object which has a key as items and then again consist of a array of Media we need to decode as below
let assets = try container.decode([[String:[Media]]].self, forKey: .assets)
// At this stage will get assets with a array of array so we first map and retrive the items and then reduce them to one single array
items = assets.compactMap{$0[CodingKeys.items.rawValue]}.reduce([], +)
}
}
At the end it's time to use it as follows
let data = jsonString.data(using: .utf8)!
let myResponse = try! JSONDecoder().decode(Response.self, from: data)
Now you can access the data as below
myResponse.name
myResponse.title
myResponse.items
Hope this basic code helps you to achieve what you want to do. And then you can go ahead and do more nested parsing(s).
I would like to thanks Nic Laughter for such a detailed article; by referring which I managed to come with above solution.
Found a new way:
struct Media: Codable {
let id,desc: String?
enum CodingKeys: String, CodingKey {
case id,desc
}
}
struct Response: Decodable, CustomStringConvertible {
let name,title: String?
#NestedKey
let items: [Media]?
enum CodingKeys: String,NestableCodingKey {
case name,title,
case items = "assets/items"
}
}
You can try this:
struct Response: Decodable {
let name, title: String
let assets: [Assets]
}
struct Assets: Decodable {
let items: [Items]
}
struct Items: Decodable {
let id, desc: String
}
Then you can just decode it like this:
guard let response = try? JSONDecoder().decode(Response.self, from: data) else { print("Response not parsed"); return }

Generic UITableViewCell with Different Kind Of JSON data

I like to use a single tableViewCell instance (instead creating separate cells) for different kind of data; used a simple generic approach by creating a protocol for this but how can I populate data from JSON (by avoiding switch-case) in clear way?
protocol CellData {
var title: String { get set }
var subTitle: String { get set }
var image: String { get set }
}
for the singleCell
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var subTitleLabel: UILabel!
#IBOutlet weak var imageView: UIImageView!
{
"data": [
{
"type": "company",
"data": {
"name": "Google",
"sector": "IT",
"logo": "https://www.google.com/logos/doodles/2015/googles-new-logo-5078286822539264.3-hp2x.gif"
}
},
{
"type": "person",
"data": {
"name": "Bill Gates",
"occupation": "Microsoft CEO",
"picture": "https://img.etimg.com/thumb/msid-66398917,width-640,resizemode-4,imgsize-702055/words-of-wisdom.jpg"
}
},
{
"type": "song",
"data": {
"name": "Beat It",
"singer": "M.Jackson",
"thumbnail": "https://cdn.smehost.net/michaeljacksoncom-uslegacyprod/wp-content/uploads/2019/08/Sept2019Mobile.jpg"
}
},
{
"type": "vehicle",
"data": {
"name": "Silver Silver",
"brand": "Silver",
"photo": "https://images.pexels.com/photos/112460/pexels-photo-112460.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500"
}
}
],
"error": null
}
If you have guarantees about the keys that you can expect, then you could try to parse the data for the first key that is found:
struct CellDataEnvelope: Decodable {
let data: CellData
let type: CellDataType
enum CellDataType: String, Decodable {
case company
case person
case song
case vehicle
}
}
struct CellData: Decodable {
let title: String
let subTitle: String
let image: String
enum CodingKeys: CodingKey {
// title
case name
// subTitle
case sector
case occupation
case singer
case brand
// image
case thumbnail
case picture
case logo
case photo
}
}
extension CellData {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.title = try container.decode(String.self, forKey: .name)
self.subTitle = try tryDecodeString(in: container, keys: [.sector, .occupation, .singer, .brand])
self.image = try tryDecodeString(in: container, keys: [.thumbnail, .picture, .logo, .photo])
}
}
// Attempts to decode a String value from an array of keys, returns the first one to successfully decode.
private func tryDecodeString(
in container: KeyedDecodingContainer<CellData.CodingKeys>,
keys: [CellData.CodingKeys]
) throws -> String {
for key in keys {
if let value = try? container.decode(String.self, forKey: key) {
return value
}
}
throw DecodingError.dataCorrupted(
.init(
codingPath: [],
debugDescription: "Invalid data"
)
)
}
let models = try JSONDecoder().decode([CellDataEnvelope].self, from: Data(json.utf8))
This could become unwieldy if the list of keys will grow substantially or if you don't have guarantees about them.
You can have a model with optional attributes, which means they won't always need to have all of the properties setted, but as mentioned by Vadian, at some point you will need to map what you are receiving.
You can also define all of your keys that are not image or title, as the subtitle (for example, sector or occupation or singer or brand).

Resources