How to parse a JSON containing dynamic keys using decodable? - ios

The problem
I am currently experiencing issues with decoding generic keys in a JSON. My current implementation accepts 3 keys primary, secondary, tertiary. However in the future I want to have the key of JSON dictionaries to be generic. I have tried to implement a similar way as stated in this tutorial: https://benscheirman.com/2017/06/swift-json/. Unfortunately I can not get it working and some help is really welcome.
My question ain't no duplicate of the below one
The following post handles a way different level of generic"nes": How to deal with completely dynamic JSON responses therefore my question is a lot more concise than the one that market this question as duplicate with the post above..
current JSON
{
"primary": {
"color": [3,111,66,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
},
"secondary": {
"color": [11,34,56,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
},
"tertiary": {
"color": [233,222,211,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
}
}
wished / possible JSON
{
"SomeKey": {
"color": [3,111,66,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
},
"OtherKey": {
"color": [11,34,56,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
},
"AnotherKey": {
"color": [233,222,211,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
}
}
The decodable structs can be found here: https://pastebin.com/ZYafkDNH
The question
How can I migrate my current code to accepts dynamic keys (at the place of primary, secondary, tertiary..) so I do not have to hard code them in the Base/Root Struct which can be found in Theme now.

You can try to parse it as a dictionary of [String:Key] instead of hardcoding the keys , by that it'll be parsed if keys are changed , but you have to do some logic inside the app to know which value corresponds to a specified key
let res = try? JSONDecoder().decode([String:Key].self, from: jsonData)
struct Key: Codable {
let color: [Int]
let font: Font
}
struct Font: Codable {
let name, size: String
}

As you seem to be responsible for the JSON I recommend to change the structure to an array and a type property.
[{"type": "primary",
"appearance": {
"color": [3,111,66,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
}
},
{
"type": "secondary",
"appearance": {
"color": [11,34,56,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
}
},
{
"type": "tertiary",
"appearance": {
"color": [233,222,211,1],
"font": {
"name": "UniversLTStd-UltraCn",
"size": "16"
}
}
}]
which is much easier to maintain.
The corresponding structs are
struct Theme : Decodable {
let type : String // could be even a custom enum
let appearance : Appearance
}
struct Appearance: Decodable {
let color: [UInt8]
let font: Font
}
struct Font: Decodable {
let name, size: String
}
and decode the JSON to [Theme].self
Otherwise as suggested by Sh_Khan you have to decode a dictionary or you have to write a custom initializer.

Related

Swift iOS how to handle JSON nested array in object class?

I would like to create a Swift TableView for all available sizes of coffee within a specific type. With this JSON tree, how can you structure the class to create an array for the type specific sizes?
The JSON Tree is structured as follows:
{
"_id": "60ba1ab72e35f2d9c786c610",
"types": [
{
"_id": "60ba1a062e35f2d9c786c56d",
"name": "Ristretto",
"sizes": [
"60ba18d13ca8c43196b5f606",
"60ba3368c45ecee5d77a016b"
],
"extras": [
"60ba197c2e35f2d9c786c525"
]
},
{
"_id": "60be1db3c45ecee5d77ad890",
"name": "Espresso",
"sizes": [
"60ba3368c45ecee5d77a016b",
"60ba33dbc45ecee5d77a01f8"
],
"extras": [
"60ba34a0c45ecee5d77a0263"
]
},
{
"_id": "60be1eabc45ecee5d77ad960",
"name": "Cappuccino",
"sizes": [
"60ba18d13ca8c43196b5f606",
"60ba3368c45ecee5d77a016b",
"60ba33dbc45ecee5d77a01f8"
],
"extras": [
"60ba197c2e35f2d9c786c525",
"60ba34a0c45ecee5d77a0263"
]
}
],
"sizes": [
{
"_id": "60ba18d13ca8c43196b5f606",
"name": "Large",
"__v": 0
},
{
"_id": "60ba3368c45ecee5d77a016b",
"name": "Venti"
},
{
"_id": "60ba33dbc45ecee5d77a01f8",
"name": "Tall"
}
],
"extras": [
{
"_id": "60ba197c2e35f2d9c786c525",
"name": "Select the amount of sugar",
"subselections": [
{
"_id": "60ba194dfdd5e192e14eaa75",
"name": "A lot"
},
{
"_id": "60ba195407e1dc8a4e33b5e5",
"name": "Normal"
}
]
},
{
"_id": "60ba34a0c45ecee5d77a0263",
"name": "Select type of milk",
"subselections": [
{
"_id": "611a1adeff35e4db9df19667",
"name": "Soy"
},
{
"_id": "60ba348d8c75424ac5ed259e",
"name": "Oat"
},
{
"_id": "60ba349a869d7a04642b41f4",
"name": "Cow"
}
]
}
]
}
My classes: (I can currently load a TableView with the coffee sizes, but not the type specific sizes)
import Foundation
import UIKit
class Types: Codable {
let types: [CoffeeType]
init(types: [CoffeeType]) {
self.types = types
}
}
class Sizes: Codable {
let sizes: [CoffeeType]
init(sizes: [CoffeeType]) {
self.sizes = sizes
}
}
class CoffeeType: Codable {
let _id: String
let name: String
init(_id: String, name: String) {
self._id = _id
self.name = name
}
}
The app is structured like this: HomeViewController is a TableView of Types. When you click on a Type, you transition to SelectSizeViewController, to which I have already assigned a Type through the segue. On SelectSizeViewController, I would like to display the list of specific sizes.
use QuickType to get the model this is quite helpful. you will be getting the model easily(suggestion). I think you are using the wrong model.
if your Base model struct is correct it should be something like this
struct Coffie: Codable {
let id: String
let types: [TypeElement]
let sizes: [Size]
let extras: [Extra]
enum CodingKeys: String, CodingKey {
case id
case types, sizes, extras
}
}
from here itself you can get the type and sizes with corresponded id's you can filter size values from sizes array

How to parse data using Alamofire when parent and child have same data?

I am using Alamofire for calling and parsing json data but issue is that response is not correct. parent and child have same data so parser changed the ids in parsing and number of items get reduced. Data below is the json which i want to parse using model
{
"error": false,
"message": "",
"data": [
{
"id": 1,
"parent_id": null,
"name": "Ink Cartridge",
"notes": null,
"children": [
{
"id": 5,
"parent_id": 1,
"name": "Colored",
"notes": null
}
]
},
{
"id": 2,
"parent_id": null,
"name": "Toner Cartridge",
"notes": null,
"children": []
},
{
"id": 3,
"parent_id": null,
"name": "Combo",
"notes": null,
"children": []
},
{
"id": 4,
"parent_id": null,
"name": "Combo Set",
"notes": null,
"children": []
}
]
}
and i am using this Alamofire and SwiftyJSON libraries
Alamofire.request(ServerAPI.getCategories()).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
print(swiftyJsonVar)
}
}
}
Try this, you should be using Codable as it makes this much easier.
Build these structs, the parent struct goes through the data using the constant name so for example let id, will get you the "id" part of the data. I then built another struct so that when it goes through children, it will do the same thing inside using that struct.
struct Parent: Codable {
let id: Int
let parent_id: Int?
let name: String
let notes: String?
let children: [Child]?
}
struct Child: Codable {
let id: Int
let parent_id: Int?
let name: String
let notes: String?
}
then you need a var to hold this data:
var completeData = [Parent]()
and in your call:
do {
let dataParsed = try JSONDecoder().decode([Parent].self, from: data
self.completeData = dataParsed
} catch {
print(error)
}
and to access it, you should be able to do
var accessingID = self.completeData[0].id
and for the child:
var accessingChild = self.completeData[0].children.id
I'm not sure how you want the data so you may want to mess around with the way you handle arrays, and I haven't been able to test but It should be along these lines.
Remember in the structs you need to use ? if its possible that the value will be null. This method also does not use Alamofire or SwiftyJSON as Codable makes it much more swifty and convenient to do this kind of thing using structs.

Filter Data in SwiftyJson

I have one SwiftyJson object.
I can not filter that array. I have tried this solution https://stackoverflow.com/a/37497170/4831567. But my json format is different that's why not working.
[
{
"name": "19860",
"header": {
"start_time": "1519270200",
"end_time": "1519299000",
"state": "lunch"
},
"alerts": "1",
"venue": {
"location": "Delhi, India",
"timezone": "+05:30"
},
"srs_category": [
0,
1
]
},
{
"name": "19861",
"header": {
"start_time": "1519270200",
"end_time": "1519299000",
"state": "Dinner"
},
"alerts": "1",
"venue": {
"location": "Mumbai, India",
"timezone": "+05:30"
},
"srs_category": [
1,
3
]
},
{
"name": "19862",
"header": {
"start_time": "1519270200",
"end_time": "1519299000",
"state": "lunch"
},
"alerts": "1",
"venue": {
"location": "Surat, India",
"timezone": "+05:30"
},
"srs_category": [
0,
2
]
}
]
i want to find that object that srs_category contain 1. I know it is possible by looping and condition. But i want via NSPredicate. If it is possible then please help me.
Thank You.
Here is easy way to use SwiftyJSON:
let filtered = JSON(yourArray).arrayValue.filter({
$0["srs_category"].arrayValue.map({ $0.intValue }).contains(1)
})
Use a Swift native function rather than NSPredicate
data represents the Data object received from somewhere
do {
if let json = try JSONSerialization.jsonObject(with:data) as? [[String:Any]] {
let srsCategory1 = json.first(where: { dict -> Bool in
guard let array = dict["srs_category"] as? [Int] else { return false }
return array.contains(1)
})
print(srsCategory1 ?? "not found")
}
} catch {
print(error)
}
If there are multiple items which can match the condition replace first with filter. Then the result is a non-optional array.

How to send JSON response to another view in Swift 3

I have a problem with sending a JSON response from UITableView into another UITableView.
I want to send the Products array like in the code below into another UITableViewController based on the selected row on the TableView, but when I log the value in didSelectRowAt it returns nil.
So here's the JSON return.
{
"error_description": [],
"results": [
{
"id": 2,
"name": "Ernser, Kilback and Kreiger LLC",
"address": "48788 Adaline Ville 7634 Bertram Shoal",
"contact_person": "Coralie Schaden",
"mobile_number": "(238) 076-0562",
"image": {
"url": null,
"thumb": {
"url": null
},
"medium": {
"url": null
},
"small": {
"url": null
},
"icon": {
"url": null
}
},
"registered_at": "2017-01-10T04:16:52.621Z",
"products": [
{
"id": 21,
"name": "Fantastic Rubber Car",
"image": {
"url": null,
"thumb": {
"url": null
},
"medium": {
"url": null
},
"small": {
"url": null
},
"icon": {
"url": null
}
},
"points": 0,
"merchant_id": 2,
"created_at": "2017-01-10T04:16:52.630Z",
"updated_at": "2017-01-10T04:16:52.636Z"
},
{
"id": 22,
"name": "Mediocre Plastic Shirt",
"image": {
"url": null,
"thumb": {
"url": null
},
"medium": {
"url": null
},
"small": {
"url": null
},
"icon": {
"url": null
}
},
"points": 15,
"merchant_id": 2,
"created_at": "2017-01-10T04:16:52.819Z",
"updated_at": "2017-01-10T04:16:52.827Z"
},
]
}
This is a part of my code for getting the response.
I used the Alamofire
var merchantModel: Merchant!
var merchantArr = [Merchant]()
var productArr = [Product]()
var productModel : Product?
.....
case .success:
let json = JSON(data: response.data!)
for result in json["results"].arrayValue{
guard let merchantID = result["id"].number, let merchantName = result["name"].string, let merchantAddress = result["address"].string, let contactPerson = result["contact_person"].string, let mobilenumber = result["mobile_number"].string else{
return
}
self.merchantModel = Merchant(merchantID: merchantID, merchantName: merchantName, merchantAddress: merchantAddress, contactPerson: contactPerson, mobileNumber: mobilenumber, merchantImage: contactPerson)
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(self.merchantModel!, toFile: Merchant.ArchiveURL.path)
if !isSuccessfulSave {
print("Failed to save sender...")
}
for prodArr in result["products"].arrayValue{
print("prodArr:\(prodArr)")
guard let prodID = prodArr["id"].number, let prodName = prodArr["name"].string,
let points = prodArr["points"].number else{
return
}
self.productModel = Product(productID: prodID, productName: prodName, productPoints: points)
}
self.merchantArr.append(self.merchantModel!)
self.productArr.append(self.productModel!)
self.merchantModel.loadMerchant()
self.tableView.reloadData()
}
Swift is not javascript, and JSON is not the simplest way to handle the data in this language.
You'd probably prefer to transform your JSON data into Foundation objects, manipulate them, create UITableView with them... And when you'll need to have the JSON format again, you transform those Foundation objects back to JSON.
There are a lot of ways to achieve that, and some amazing third-party libraries to do it very easily. However for a start I encourage you to have a look at Apple's resources :
NSJSONSerialization
Working with JSON in Swift

ElasticSearch: Multi field "upgrade" yields error:

Okay, here is the task:
I've already read the whole documentation and I noticed that I can "upgrade" a data type like string to a multi field - in a test scenario it already worked.
My documents structure is currently:
{
"name": "test",
"words": [
{
"words": "hello world",
"verts": [
1,
2,
3
]
}
]
}
These documents were created using the default mappings - so no mapping has been set explicitly.
I am issuing a XDELETE command with data like:
{
"article": {
"properties": {
"words": {
"type": "multi_field",
"fields": {
"words": {
"type": "string",
"index": "analyzed"
},
"untouched": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}
}
But I receive this error here:
{"error":"MergeMappingException[Merge failed with failures {[Can't
merge a non multi_field / non simple mapping [words] with a
multi_field mapping [words]]}]","status":400}
Can someone explain to me, why this happens? When I issue this mapping to a clean index, it works and the not_analyzed filter is being applied.
Thanks :)
Jan
Because the "words" field in your document has properties of its own ("words" and "verts"), you can't "upgrade" it to a multi_field. However, if you had a mapping like
{
"article": {
"properties": {
"words": {
"properties": {
"words": {
"type": "multi_field",
"fields": {
"words": {
"type": "string",
"index": "analyzed"
},
"untouched": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}
}
}
}
then everything should work out.

Resources