Swift : How to create a dictionary dynamically from a json? - json

I'd like some advice from you. I would like to create a dictionary from a dynamic response fetch from an API and send that dictionary in an Alamofire POST request.
From what I have so far it's working but I'm not satisfied with what i've made and I think the code is really messy.
Here is an example of what I can receive
"content": {
"type": "form",
"fields": [
{
"type": "select",
"label": "Do you have your documents?",
"field": "user.has_docs",
"default": 0,
"choices": [
{
"value": 0,
"name": "Not yet"
},
{
"value": 1,
"name": "I do"
}
]
},
{
"type": "input",
"field": "user.date",
"label": "When do you arrive?",
}
]
}
After parsing the json with the Codable protocol, I have all my data in the Model Field
type: String
label: String
field: String
defaultValue: Int?
choice: [Choice]?
Choice
value: Int
name: String
So I want to create my dictionary and I want the following scheme :
{
"value": {
"user": {
"has_docs": 1,
"date": "29/07/2020"
}
}
}
The key named : "value" is always the same value, but the other one depends of the result from the API. the prefix of the field corresponding of "parent object" and the right part is the child.
Hard coding a dictionary in Swift is not that hard, I would do
let dict = [
"value": [
"user": [
"has_docs": 1,
"date": "29/07/2020"
]
]
]
But the troubles begin, at the attempt of creating a dictionary dynamically. Values inside user keep only the last one and replacing has_docs with date.
I have found a workaround with using flatmap and reduce but it only allows the type [String: String], unfortunately I need to write [String: Int] too in the dictionary.
here is a sample of the code
let flattenedDictionary = [key : dictionaries
.flatMap { $0 }
.reduce([String:String]()) { (dict, tuple) in
var nextDict = dict
nextDict.updateValue(tuple.1 as! String, forKey: tuple.0)
return nextDict
}]
parameters["value"] = flattenedDictionary
Here :
key = "user".
dictionaries = [["has_docs": 1], ["date": "29/07/2020"]]
Feel free to exchange if you need more informations
If you have any clue on how you could helping me, I'll highly appreciate, thanks for reading so far.
I hope I was very understandable.
Edit
From a general view : I'd like to create a dictionary dynamically
[String: [String: [String: Any]]]

A bit unclear if you have a [String: [String: [String: Any]]] or [String: [String: Any]] dictionary, but the concept of creating it dynamically would be rather similar.
var user: [String: Any] = [:]
user["has_docs"] = 1
user["date"] = Date()
let dict = ["value": user]

Related

JSON Array Swift send using Alamofire

I am trying to send an array of JSON object which I need to update object on my server. I need the array for params request.
This is my construction of array I need in order to update server object value:
[{
"propName":"number", "value":numberValue
}]
Here is what I am trying to make in Swift :
let params = [
{ "propName":"number", "value":numberValue },
{ "propName":"address", "value":addressValue },
{ "propName":"notes", "value":notesValue },
{ "propName":"latitude", "value":latitudeValue },
{ "propName":"longitude", "value":longitudeValue}
] as [String: Any]
let updateParkingSpotRequest = AF.request(URLs.MarkParkingSpotAsAvail(parkingSpotId: parkingSpotId), method: .patch, parameters: params, encoding: JSONEncoding.prettyPrinted, headers: nil, interceptor: nil, requestModifier: nil)
But it doesn't work since it cannot convert this form of data, XCode says: "Cannot convert value of type '[() -> String]' to type '[String : Any]' ". How can I get that format of Data which server needs?
You need to write the initializer like this:
let params = [
[ "propName": "number", "value": numberValue ],
[ "propName": "address", "value": addressValue ],
[ "propName": "notes", "value": notesValue ],
[ "propName": "latitude", "value": latitudeValue ],
[ "propName": "longitude", "value": longitudeValue ]
] as [[String: Any]]
i.e. params is an Array of Dictionaries of String to Any.
The parameters parameter is a type of [String: Any] in Alamofire. So you need to create a parameter dictionary.
So assume that you have a list of Data models, you can create the params like below;
struct Data {
var propName: String
var value: Any
}
let values: [Data] = [
Data(propName: "number", value: numberValue),
Data(propName: "address", value: addressValue),
Data(propName: "notes", value: notesValue),
Data(propName: "latitude", value: latitudeValue),
Data(propName: "longitude", value: longitudeValue)
]
let params: [String: Any] = Dictionary(uniqueKeysWithValues: values.map{ ($0.propName, $0.value) })

Parsing a nested JSON using Decodable

My JSON structure looks like this:
{
"code": 200,
"status": "Ok",
"etag": "7232324423",
"data": {
"offset": 0,
"limit": 25,
"results": [{
"id": 1011244,
"name": "Miss Nesbit",
"description": "",
"modified": "2018-04-04T20:15:35-0400",
"thumbnail": {
"path": "http://i.annihil.us/u/prod/i/mg/8/70/4c002efc322e3",
"extension": "jpg"
}
},
{
"id": 1011244,
"name": "Miss Solis",
"description": "",
"modified": "2018-09-04T20:15:35-0400",
"thumbnail": {
"path": "http://i.annihil.us/u/prod/i/mg/8/70/4c002efc382e3",
"extension": "jpg"
}
}
]
}
}
I want to parse the results in a struct as follows:
struct Character: Codable {
let id: Int
let name: String
let thumbnail: Thumbnail
let description: String
}
However I'm a bit confused about where I specify I only want the results part ? Would I do it when implementing Decodable as follows?
let container = try decoder.container(keyedBy: CodingKeys.self)
let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data)
let results = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .results)
Or do we have to map out each nested section? ANy help or guidance would be so appreciated! :)
Mapping out the relevant keys is necessary to drill down for you, yes.
You can use app.quicktype.io to get started fast and remove the non-relevant keys if you really don‘t want to parse the rest or leave it there if you may want to use it later.
You can use my extension helper NestedDecodable, which allows you to extract Decodable from a keyPath. It adds this method to JSONDecoder:
decoder.decode(Class, from: Data, keyPath: String)
A key path specifies the nested path to the model you want, separated by .
So using your example, this works (tested):
let characters = try decoder.decode([Character].self, from: data, keyPath: "data.results")
Internally, it creates a wrapper struct to hold your target Decodable class, split keyPath by ., then for-loop each key down until it reaches the final keyed decoding container, decode that model and returns. Ref

JSONDecoder changing order of elements [duplicate]

I am trying to create a dictionary that I can make into a JSON formatted object and send to the server.
Example:
var users = [
[
"First": "Albert",
"Last": "Einstein",
"Address":[
"Street": "112 Mercer Street",
"City": "Princeton"]
],
[
"First": "Marie",
"Last": "Curie",
"Address":[
"Street": "108 boulevard Kellermann",
"City": "Paris"]]
]
I use this function
func nsobjectToJSON(swiftObject: NSObject) -> NSString {
var jsonCreationError: NSError?
let jsonData: NSData = NSJSONSerialization.dataWithJSONObject(swiftObject, options: NSJSONWritingOptions.PrettyPrinted, error: &jsonCreationError)!
var strJSON = NSString()
if jsonCreationError != nil {
println("Errors: \(jsonCreationError)")
}
else {
// everything is fine and we have our json stored as an NSData object. We can convert into NSString
strJSON = NSString(data: jsonData, encoding: NSUTF8StringEncoding)!
println("\(strJSON)")
}
return strJSON
}
But my result is this:
[
{
"First" : "Albert",
"Address" : {
"Street" : "112 Mercer Street",
"City" : "Princeton"
},
"Last" : "Einstein"
},
{
"First" : "Marie",
"Address" : {
"Street" : "108 boulevard Kellermann",
"City" : "Paris"
},
"Last" : "Curie"
}
]
Problem: why is the last name last? I think it should be above address. Please let me know what I am doing wrong with the NSDictionary for this to come out wrong. Any help would be very much appreciated - thank you.
To post what has already been said in comments: Dictionaries are "unordered collections". They do not have any order at all to their key/value pairs. Period.
If you want an ordered collection, use something other than a dictionary. (an array of single-item dictionaries is one way to do it.) You can also write code that loads a dictionary's keys into a mutable array, sorts the array, then uses the sorted array of keys to fetch key/value pairs in the desired order.
You could also create your own collection type that uses strings as indexes and keeps the items in sorted order. Swift makes that straightforward, although it would be computationally expensive.
I did like this.
let stagesDict = NSDictionary()
if let strVal = sleepItemDict["stages"] as? NSDictionary {
stagesDict = strVal
let sortedKeys = (stagesDict.allKeys as! [String]).sorted(by: <)
var sortedValues : [Int] = []
for key in sortedKeys {
let value = stagesDict[key]!
print("\(key): \(value)")
sortedValues.append(value as! Int)
}
}

Parsing JSON response in Swift 3

I've got an API endpoint that returns JSON in the following format:
[
{
"id": "1",
"name": "John"
},
{
"id": "2",
"name": "Jane"
},
{
"id": "3",
"name": "Nick"
}
]
I am trying to parse this in Swift 3, but I can only find examples to parse JSON formatted like so:
{
"blogs": [
{
"needspassword": true,
"id": 73,
"url": "http://remote.bloxus.com/",
"name": "Bloxus test"
},
{
"needspassword": false,
"id": 74,
"url": "http://flickrtest1.userland.com/",
"name": "Manila Test"
}
],
"stat": "ok"
}
, which has an extra level above what mine does.
So, where examples I've seen are simply parsing their data like jsonResponse["blogs"], I can't do that as my format is different.
How can I parse the format I've got, or how can I return a format that is easier to parse?
Any suggestions appreciated, thanks!
You can just do the following :
let data = // Data received from WS
do {
let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) as? [[String : String]]
//json is now an array from dictionary matching your model
}
catch {
//handle error
}
This will parse it when placed in the network call.
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions()) as! [[String : AnyObject]]
let firstPerson = json[0]
print(firstPerson)
let id = firstPerson["id"] as! String
print(id)
let name = firstPerson["name"] as! String
print(name)
} catch {
//handle error
}
Also, I tend to be against advising third party libraries, but SwiftyJSON is an exception I make. If you want to try it, add this to your pod file:
pod SwiftyJSON', '3.0.0'
Documentation: https://github.com/SwiftyJSON/SwiftyJSON
EDIT - Answering Comment:
Replacement line:
if let id = firstPerson["id"] as? String {
print(id)
}
Replacement line (if you need to hold on to the value):
var thisId: String?
if let id = firstPerson["id"] as? String {
thisId = id
}
print(thisId ?? "")
i don't really know swift but there might be the equivalent of ajax json encoding (server side you json_encode your array and client side you json_decode the response)
the idea is to have the same formatter that encodes and decodes the data

Parsing JSON with Mantle - Swift

I'm using mantle to parse JSON data with Swift. The content of the JSON file consists of:
{
"Name1": [
{
"Type": "New",
"Available": true,
"Kind": "4178228729",
"Loot": "4367",
"Advanced": [
{
"Type": "Old",
"Name": "RoundRobin",
"Available": true,
"Specs": [
{
"Type": "Fire",
"Available": true,
"Actions": [
--continues with similar pattern--
],
"Name2": [
--repeats the same pattern at before--
]
}
I created the model classes, inheriting from MTLJSONSerializing. The "first level class" is to contain the two top level arrays of objects.
import Foundation
class lv1Class: MTLJSONSerializing {
let name1: Array<lv2Class> = []
let name2: Array<lv2Class> = []
class func name2JSONTransformer() -> NSValueTransformer {
return NSValueTransformer.mtl_JSONArrayTransformerWithModelClass(lv2Class.self)
}
class func name1JSONTransformer() -> NSValueTransformer {
return NSValueTransformer.mtl_JSONArrayTransformerWithModelClass(lv2Class.self)
}
override class func JSONKeyPathsByPropertyKey() -> [NSObject : AnyObject]!
{
return ["name1": "Name1",
"name2": "Name2"]
}
}
And it goes on like this deeper and deeper.
import Foundation
class lv2Class: MTLJSONSerializing {
let type: String = ""
let available: Bool = true
let kind: String = ""
let loot: String = ""
let advanced: Array<lv3Class> = []
class func advancedJSONTransformer() -> NSValueTransformer {
return NSValueTransformer.mtl_JSONArrayTransformerWithModelClass(lv3Class.self)
}
override class func JSONKeyPathsByPropertyKey() -> [NSObject : AnyObject]!
{
return ["type": "Type",
"available": "Available",
"kind": "Kind",
"loot": "Loot",
"advanced": "Advanced"]
}
}
The other classes are similar in structure. (I can post them all on gists if necessary.)
The call to get the JSON data is as follows:
if let lv1ClassObject = MTLJSONAdapter.modelOfClass(lv1Class.self, fromJSONDictionary: testDic, error: errorjson) as? lv1Class
testDic is the file posted above.
The call that i make to get the json data succeeds, I'm pretty confident that the problem is in my data model. The value of the dictionary is an array so I feel pretty confident parsing it as an array.
The error I get is:
Assertion failure in -[MTLJSONAdapter initWithJSONDictionary:modelClass:error:], /Users/xx/Documents/Xcode/My_project/Pods/Mantle/Mantle/MTLJSONAdapter.m:149
2015-03-14 14:34:20.331 My_project[25794:711328] *** Caught exception available is not a property of My_project.lv2Class
But available is a property of such class...
let available: Bool = true
Hope it's clear enough, if you have any question feel free to ask.
I'm really not familiar with parsing JSON, thanks everyone for the help.
Answer is here.
You both need to put them as dynamic and to make sure that all your property can be bridged to the equivalent Obj-C property.