Complex JSON to Swift - json

So I am relatively new to JSON and converting it to Swift. I have only dealt with basic JSON with simple data types. Nothing fancy. I also have only dealt with using JSON that I have created.
Now I am trying to use JSON that I have gathered from and API. This JSON has nested objects inside of other objects.
here is the format of the JSON:
{
"records": [
{
"id": "Info",
"fields": {
"Card Name": "Info",
"Qty": 0,
"Card Type": "Info",
"Color": "Info",
"Card #": "Info",
"Rarity": "Info",
"Image": [
{
"id": "Info",
"url": "url",
"filename": "info",
"size": 0,
"type": "info",
"thumbnails": {
"small": {
"url": "url",
"width": 0,
"height": 0
},
"large": {
"url": "url",
"width": 0,
"height": 0
},
"full": {
"url": "url",
"width": 0,
"height": 0
}
}
}
],
"Level": 0,
"Full Set": 0
},
"createdTime": "info"
}
],
"offset": "info"
}
I am not sure if I need to do anything with the "records": portion. Now there are more records within the "records": array.
I don't need all of the information within each record.
I tried doing something like:
struct Card {
let offSet: String
let cardName: String
}
extension Card: Decodable {
enum CodingKeys: String, CodingKey {
case offSet
case cardName
enum FieldKeys: String, CodingKey {
case cardName = "Card Name"
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
offSet = try container.decode(String.self, forKey: .offSet)
let fieldContainer = try container.nestedContainer(keyedBy: CodingKeys.FieldKeys.self, forKey: .cardName)
cardName = try fieldContainer.decode(String.self, forKey: .cardName)
}
}
Every time I try to decode the info into my Swift Struct it just comes up nil. Not sure what I am doing wrong or where to go from here. I haven't dealt with JSON that have "records": setup like this. Not sure if this even matters.
Also a side question in regards to images. Would I just use the url property to display the image? The image files will be PNG. Never dealt with images and JSON before.

As Larme commented "Your code can't guess" - Meaning you need to represent the same structure in code as your JSON provides.
In danger of doing the work for you I've written a very verbose set of Codable struct's that pull out all of the data from the JSON you provided.
struct Card: Codable {
var records:[Record]
var offset:String
}
struct Record: Codable {
var id:String
var fields:Fields
var createdTime:String
}
struct Fields: Codable {
var name:String
var quantity:Int
var type:String
var color:String
var number:String
var rarity:String
var level:Int
var fullSet:Int
var image:[Image]
enum CodingKeys: String, CodingKey {
case name = "Card Name"
case quantity = "Qty"
case type = "Card Type"
case color = "Color"
case number = "Card #"
case rarity = "Rarity"
case level = "Level"
case fullSet = "Full Set"
case image = "Image"
}
}
struct Image: Codable {
var id:String
var url:String
var filename:String
var size:Int
var type:String
var thumbnails:Thumbnails
}
struct Thumbnails: Codable {
var small:Thumbnail
var large:Thumbnail
var full:Thumbnail
}
struct Thumbnail: Codable {
var url:String
var width:Int
var height:Int
}
I tested the above in a playground by pasting the JSON you provided as a multiline string and using the following to dump the resulting objects.
do {
if let data = json.data(using: .utf8) {
let card = try JSONDecoder().decode(Card.self, from: data)
dump(card)
}
} catch {
fatalError("Failed to decode \(error)")
}
The output of which was:
▿ __lldb_expr_21.Card
▿ records: 1 element
▿ __lldb_expr_21.Record
- id: "Info"
▿ fields: __lldb_expr_21.Fields
- name: "Info"
- quantity: 0
- type: "Info"
- color: "Info"
- number: "Info"
- rarity: "Info"
- level: 0
- fullSet: 0
▿ image: 1 element
▿ __lldb_expr_21.Image
- id: "Info"
- url: "url"
- filename: "info"
- size: 0
- type: "info"
▿ thumbnails: __lldb_expr_21.Thumbnails
▿ small: __lldb_expr_21.Thumbnail
- url: "url"
- width: 0
- height: 0
▿ large: __lldb_expr_21.Thumbnail
- url: "url"
- width: 0
- height: 0
▿ full: __lldb_expr_21.Thumbnail
- url: "url"
- width: 0
- height: 0
- createdTime: "info"
- offset: "info"
Once you have all the data you could start simplifying things, perhaps an extension to the Card that gets the first records name.
extension Card {
var cardName:String {
guard let firstRecord = self.records.first else {
return "Unknown Name"
}
return firstRecord.fields.name
}
}
print(card.cardName) // "Info\n"

Related

How to decode this JSON with dynamic keys (on the root level) in Swift?

I am trying to create a small app to control my Hue lights with SwiftUI, but I just can't manage to decode/iterate through this JSON. There are so many threads on how to do this and I have tried tens of them, using CodingKeys, creating custom decoders, and so on, but I just can't seem to get it. So this is the JSON I am getting from my Hue Bridge:
{
"1": {
"state": {
"on": true,
"bri": 254,
"hue": 8417,
"sat": 140,
"effect": "none",
"xy": [
0.4573,
0.41
],
"ct": 366,
"alert": "select",
"colormode": "ct",
"mode": "homeautomation",
"reachable": false
},
"swupdate": {
"state": "noupdates",
"lastinstall": "2021-08-26T12:56:12"
},
...
},
"2": {
"state": {
"on": false,
"bri": 137,
"hue": 36334,
"sat": 203,
"effect": "none",
"xy": [
0.2055,
0.3748
],
"ct": 500,
"alert": "select",
"colormode": "xy",
"mode": "homeautomation",
"reachable": true
},
"swupdate": {
"state": "noupdates",
"lastinstall": "2021-08-13T12:29:48"
},
...
},
"9": {
"state": {
"on": false,
"bri": 254,
"hue": 16459,
"sat": 216,
"effect": "none",
"xy": [
0.4907,
0.4673
],
"alert": "none",
"colormode": "xy",
"mode": "homeautomation",
"reachable": true
},
"swupdate": {
"state": "noupdates",
"lastinstall": "2021-08-12T12:51:18"
},
...
},
...
}
So the structure is basically a dynamic key on the root level and the light info, that is the same. I have created the following types:
struct LightsObject: Decodable {
public let lights: [String:LightInfo]
}
struct LightInfo: Decodable {
var state: StateInfo
struct StateInfo: Decodable {
var on: Bool
var bri: Int
var hue: Int
var sat: Int
var effect: String
var xy: [Double]
var ct: Int
var alert: String
var colormode: String
var reachable: Bool
}
var swupdate: UpdateInfo
struct UpdateInfo: Decodable {
var state: String
var lastinstall: Date
}
...
(it basically continues to include all the variables from the object)
The problem I have now, is that I can't seem to get this into a normal array or dictionary. I would settle for something like {"1":LightInfo, "2", LightInfo}, where I could iterate over, or just a simple [LightInfo, LightInfo, ...], because I may not even need the index.
And then, ideally, I could do something like
ForEach(lights) { light in
Text(light.name)
}
I have tried creating a custom coding key, implementing the type as Codable, and so on, but I couldn't find a solution, that works for me. I know, there are a lot of threads on this topic, but I feel that my initial setup might be wrong and that's why it's not working.
This is the decoding part:
let task = urlSession.dataTask(with: apiGetLightsUrl, completionHandler: { (data, response, error) in
guard let data = data, error == nil else { return }
do {
let lights = try JSONDecoder().decode([String: LightInfo].self, from: data)
completionHandler(lights, nil)
} catch {
print("couldn't get lights")
completionHandler(nil, error)
}
})
I am actually getting the JSON, no problem, but I have not been able to decode it, yet, as I said. The latest error being:
Optional(Swift.DecodingError.typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "1", intValue: nil), CodingKeys(stringValue: "swupdate", intValue: nil), CodingKeys(stringValue: "lastinstall", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil)))
I have seen some posts, but the JSON they were working with usually had a top-level object, something like
{
"foo":
{
"bar": "foobar"
...
},
{
"bar": "foobar2"
...
},
{
"bar": "foobar3"
...
}
...
}
So the handling of that was a little different, since I could just create a struct like
struct Object: Decodable {
var foo: [LightsInfo]
}
which is not possible here :(
Can anybody point me in the right direction? Thanks!
Update:
Thanks guys, the solutions do work. I had some errors in my struct, which are now fixed. (misspelled variable names, optional values and so on). The only thing missing now, is how to loop through the dictionary. If I use
ForEach(lights)...
Swift complains, that it is only supposed to be used for static Dictionaries. I tried doing this
ForEach(lights, id: \.key) { key, value in
Button(action: {print(value.name)}) {
Text("foo")
}
}
but I get this error: Generic struct 'ForEach' requires that '[String : LightInfo]' conform to 'RandomAccessCollection'
Update 2:
So this seems to work:
struct LightsView: View {
#Binding var lights: [String:LightInfo]
var body: some View {
VStack {
ForEach(Array(lights.keys.enumerated()), id: \.element) { key, value in
LightView(lightInfo: self.lights["\(value)"]!, lightId: Int(value) ?? 0)
}
}
}
}
I'll try to clean up the code and optimize it a bit. Open for suggestions ;)
You seem to be almost there. This is a [String:LightInfo]. You just need to decode that (rather than wrapping that up in a LightsObject that doesn't exist in the JSON). You can pull off the values if you don't care about the numbers. It should be like this:
let lights = try JSONDecoder().decode([String: LightInfo].self, from: data).values
I ran into a similar problem myself where I wanted to handle generic keys given. This is how I solved it, adapted to your code.
struct LightsObject: Decodable {
public var lights: [String:LightInfo]
private enum CodingKeys: String, CodingKey {
case lights
}
// Decode the JSON manually
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.lights = [String:LightInfo]()
if let lightsSubContainer = try? container.nestedContainer(keyedBy: GenericCodingKeys.self, forKey: .lights) {
for key in lightsSubContainer.allKeys {
if let lightInfo = try? lightsSubContainer.decode(LightInfo.self, forKey: key) {
self.lights?[key.stringValue] = lightInfo
}
}
}
}
}
public class GenericCodingKeys: CodingKey {
public var stringValue: String
public var intValue: Int?
required public init?(stringValue: String) {
self.stringValue = stringValue
}
required public init?(intValue: Int) {
self.intValue = intValue
stringValue = "\(intValue)"
}
}
If you are only interested in the values of the root dictionary add a custom initializer
struct LightsObject: Decodable {
let lights: [LightInfo]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String:LightInfo].self)
lights = Array(dictionary.values)
}
}
and decode
let result = try JSONDecoder().decode(LightsObject.self, from: data)
completionHandler(result.lights, nil)
which returns an array of LightInfo objects and you can declare
#Binding var lights: [LightInfo]
Of course the completion handler type must be ([LightInfo]?, Error?) -> Void. Anyway consider to use the more convenient Result type

Parsing JSON in Swift without array key

I have a JSON-response:
[
[{
"id": "1",
"name": "Toyota",
"model": "Camry"
},
{
"id": "2",
"name": "Nissan",
"model": "Almera"
}
],
{
"count": "1234",
"page": "1"
}
]
I create decodable model:
struct Car: Decodable {
var id: String?
var name: String?
var model: String?
}
I'm trying extract data like this, for test:
let carsResponse = try JSONDecoder().decode([[Car]].self, from: data)
print(carsResponse[0])
And I have an error:
Expected to decode Array but found a dictionary instead.
What is wrong?
This format is pretty bad, so you'll need to decode the outer container by hand, but it's not difficult:
struct CarResponse: Decodable {
var cars: [Car]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
cars = try container.decode([Car].self) // Decode just first element
}
}
let carsResponse = try JSONDecoder().decode(CarResponse.self, from: data)
print(carsResponse.cars)

How to create a Swift model for JSON

{"dataList":{"1547795650562": {
"c0a8007b-6759-111d-8167-59e8dabe0086": {
"recordDate": 1547795650562,
"resultValue": "160",
"vitalParameter": {
"uom": {
"code": "KG",
"name": "KG",
"id": "c0a8007b-6759-111d-8167-59e76204007f"
},
"resultType": {
"code": "VSRTNUMERIC",
"name": "Numeric",
"id": "20cf4756-40b0-4cc1-acb5-861765370a41"
},
"code": "29463-7",
"name": "Weight",
"id": "c0a8007b-6759-111d-8167-59e8dabe0086"
},
"id": "c0a8007b-6855-1d16-8168-5fd18fa301b7"
}}
}}
getting 1547795650562 and c0a8007b-6759-111d-8167-59e8dabe0086 as class names. But I dont want like this;
class DataList : NSObject, NSCoding{
var 1547795650562 : 1547795650562!
}
class 1547795650562 : NSObject, NSCoding{
var c0a8007b6759111d816759e8dabe0086 : VitalParameter!
}
But the problem here is, 1547795650562 and c0a8007b-6759-111d-8167-59e8dabe0086 cannot be hard coded because they may change.
c0a8007b-6759-111d-8167-59e8dabe0086 is dynamic id and 1547795650562 is recordDate. Inner object is repetitive.
But I have to map as the keys are of recordDate and id respectively.
Try using Codable instead of NSCoding to parse your JSON data.
Models:
struct Root: Codable {
let dataList: [String:[String:Record]]
}
struct Record: Codable {
let recordDate: Int
let resultValue: String
let vitalParameter: VitalParameter
let id: String
}
struct VitalParameter: Codable {
let uom, resultType: ResultType
let code, name, id: String
}
struct ResultType: Codable {
let code, name, id: String
}
Parse the JSON data using above models like,
do {
let response = try JSONDecoder().decode(Root.self, from: data)
print(response)
} catch {
print(error)
}
Note: You can use https://app.quicktype.io to get the models from your JSON instantly. Make the changes as per your requirement and you're good to go.

read JSON result as array of dictionary

how to parse following JSON result
{
"AddedByName": "jhon",
"ApproveAction": 0,
"ApproveActionName": "",
"photos": null,
"Status": 0,
},
{
"AddedByName": "mike",
"ApproveAction": 0,
"ApproveActionName": "",
"photos": null,
"Status": 0,
},
{
"AddedByName": "someone",
"ApproveAction": 0,
"ApproveActionName": "",
"photos": [
{
"Id": 53,
"Serial": 1,
"Url": "0afe88a3-76e1-4bac-a392-173040936300.jpg"
}
],
"Status": 0,
}
how can i reach the "photos" array ?
I already declare local array of dictionary to hold the whole responses as following
var myLocalArray = [[String:Any]]()
and fill it from the JSON response like this
if let Json = response.result.value as? [String:Any] {
if let ActionData = Json["ActionData"] as? [[String:Any]] {
self. myLocalArray = ActionData
}
}
and it works
but i couldn't reach the "photos" array please help
I will give you smart solution than can be useful at each you try to transform you Json to useful data model and make it easier to manipulate .
Using the power of Decodable .
This Model will help you to capture you Json
struct User: Decodable {
var AddedByName: String
var ApproveAction: Int
var ApproveActionName: String
var photos: [Photo]?
var Status: Int
struct Photo: Decodable {
var Id: Int
var Serial: Int
var Url: String
}
}
And now will be just one line to get your Json to structure data:
let responseData = try JSONDecoder().decode([User].self, from: jsonD)
#swiftIos provided the answer with Decodable which is absolutely a better way to handle the situation.
But with your current you can access the photos from self.myLocalArray:
if let jsonData = response.result.value as? [String: Any] {
if let actionData = jsonData["ActionData"] as? [[String:Any]] {
self.myLocalArray = actionData
}
}
Now you have array for actionData, so access the photos by extracting the actionData for particular index as self.myLocalArray[0]. In whole:
let index = 0
if self.myLocalArray.count > index {
if let photoArrayIndex0 = self.myLocalArray[index]["photos"] as? [[String: Any]] {
print(photoArrayIndex0)
}
}

Creating Model using Swifty JSon Model

[
{
"_id": "1212323",
"row": 1,
"column": 1,
"displayType": 0,
"item": {
"type": "category_",
"data": {
"_id": "595a1446cb91951900b0b4b0",
"title": "something",
"fullImage": "http://assets.something.mobi/curated/something (2).jpg",
"halfImage": "http://assets.something.mobi/curated/something (1).jpg"
}
}
},
{
"_id": "595a148ccb91951900b0b4b5",
"row": 2,
"column": 1,
"displayType": 1,
"item": {
"type": "curatedlist",
"data": {
"_id": "595b34abcb9195190c0ae378",
"active": "true",
"title": "sample something list",
"fullImage": "http://assets.something.mobi/curated/something (2).jpg",
"halfImage": "http://assets.something.mobi/curated/something (2).jpg"
}
}
}
]
very new to SwiftyJSON and Alamofire some one please help me to create a model for it im able get JSON responce using almofire but not able to create a proper swifty json model for this
I have done this using two separate model classes, one to directly map all the values and other used to fetch directly as an array from it.
For some who just starting or stuck with creating model and mapping values can try these or someone is an expert can suggest a better solution to improve the implementation
1.CategoryModel: NSObject
import UIKit
import SwiftyJSON
class CategoryModel: NSObject {
var resultArray : NSArray!
var _id : String!
var row : NSInteger!
var column : NSInteger!
var displayType : NSInteger!
var type : String?
var _id1 : String?
var title : String?
var fullImage : String?
var halfImage : String?
// var item
required init(JsonDashBoard: JSON) {
_id = JsonDashBoard["_id"].stringValue
row = JsonDashBoard["row"].intValue
column = JsonDashBoard["column"].intValue
displayType = JsonDashBoard["displayType"].intValue
//MARK:- Inside Item Dictionary
type = JsonDashBoard["item"]["type"].stringValue
//MARK:- Inside Item/dataDictionary
_id1 = JsonDashBoard["item"]["data"]["_id"].stringValue
title = JsonDashBoard["item"]["data"]["title"].stringValue
fullImage = JsonDashBoard["item"]["data"]["fullWidthImage"].stringValue
halfImage = JsonDashBoard["item"]["data"]["halfWidthImage"].stringValue
}
}
2.CategoryListModel:NSObject
import UIKit
class CategoryListModel: NSObject {
var items : NSArray? = nil;
required init(_items: NSMutableArray) {
self.items = _items
}
}
and using these model classes you can map values using the below code snipet
manager.request(routeUrl, method: .get).responseJSON { (responseObject) -> Void in
if responseObject.result.isSuccess {
let resJson = JSON(responseObject.result.value!)
let categoryList = NSMutableArray()
for (_,subJson):(String, JSON) in resJson {
let model = CategoryModel(JsonDashBoard: subJson)
categoryList.add(model)
//Do something you want
print(subJson)
}
let categoryListModel = CategoryListModel(_items: categoryList)
success(categoryListModel)
}
if responseObject.result.isFailure {
let error : Error = responseObject.result.error!
failure(error)
}
}