Decode JSON single object or array of object dynamically [duplicate] - json

This question already has an answer here:
Swift 4 JSON Codable - value returned is sometimes an object, others an array
(1 answer)
Closed 3 years ago.
Let's say I have an array of JSON response from the GET method like :
[{
"id":"1",
"Name":"John Doe",
},{
"id":"2",
"Name":"Jane Doe",
}]
And from the POST method using id param I only have 1 object JSON response :
{
"id":"1",
"Name":"John Doe",
}
how can I write a method to decode both the JSON dynamically?
At the moment, this is what I'm using :
func convertJSON<T:Decodable>(result: Any?, model: T.Type) -> T? {
if let res = result {
do {
let data = try JSONSerialization.data(withJSONObject: res, options: JSONSerialization.WritingOptions.prettyPrinted)
return try JSONDecoder().decode(model, from: data)
} catch {
print(error)
return nil
}
} else {
return nil
}
}
The method can be used to decode a single object using dynamic model, but I just can't figure it out to handle a single object / an array of objects dynamically.
The most I can get with is just using a duplicate of the method but replacing T with
[T] in the method parameter and return type, if the response is an array.
I'm open to any suggestion, any help is appreciated, Thank You in advance.
Edit : If this question is duplicate of this , I'm not sure how the marked answer could be a solution.

One solution could be to always return [Model]?.
Inside your function first try to decode as Model, on success return an array with that single decoded object inside it. If this fails then try to decode as [Model], on success return the decoded object else return nil.
Using your sample JSONs I created a struct:
struct Person: Codable {
let id, name: String
enum CodingKeys: String, CodingKey {
case id
case name = "Name"
}
}
Then I created a struct with a couple of methods to decode from either a String or an optional Data.
struct Json2Type<T: Decodable> {
// From data to type T
static public func convertJson(_ data: Data?) -> [T]? {
// Check data is not nil
guard let data = data else { return nil }
let decoder = JSONDecoder()
// First try to decode as a single object
if let singleObject = try? decoder.decode(T.self, from: data) {
// On success return the single object inside an array
return [singleObject]
}
// Try to decode as multiple objects
guard let multipleObjects = try? decoder.decode([T].self, from: data) else { return nil }
return multipleObjects
}
// Another function to decode from String
static public func convertJson(_ string: String) -> [T]? {
return convertJson(string.data(using: .utf8))
}
}
Finally call the method you prefer:
Json2Type<Person>.convertJson(JsonAsDataOrString)
Update: #odin_123, a way to have either a Model or [Model] as return value can be accomplish using an enum. We can even add the error condition there to avoid returning optionals. Let's define the enum as:
enum SingleMulipleResult<T> {
case single(T)
case multiple([T])
case error
}
Then the struct changes to something like this:
struct Json2Type<T: Decodable> {
static public func convertJson(_ data: Data?) -> SingleMulipleResult<T> {
guard let data = data else { return .error }
let decoder = JSONDecoder()
if let singleObject = try? decoder.decode(T.self, from: data) {
return .single(singleObject)
}
guard let multipleObjects = try? decoder.decode([T].self, from: data) else { return .error }
return .multiple(multipleObjects)
}
static public func convertJson(_ string: String) -> SingleMulipleResult<T> {
return convertJson(string.data(using: .utf8))
}
}
You can call it the same way we did before:
let response = Json2Type<Person>.convertJson(JsonAsDataOrString)
And use a switch to check every possible response value:
switch(response) {
case .single(let object):
print("One value: \(object)")
case .multiple(let objects):
print("Multiple values: \(objects)")
case .error:
print("Error!!!!")
}

Related

Swift: JSONDecoder returning nil from API

currently working through an app that gets and decodes data from OpenWeatherMap API, currently I've got everything working except getting the decoder to return something. Currently, the decoder is returning nil, however, I am getting bytes of data from the API call. I am not exactly sure what could be the issue. I've got the ViewModel struct set up in terms of hierarchy. The OPW API JSON data seems to be in the format of a dictionary key:value pair collection type, keys are enclosed in quotes, could it be that my decoder isn't finding the necessary information because of the quotation marks?
Getting and Decoding the API call...
#IBAction func saveCityButtonPressed() {
if let city = cityNameTextField.text {
let weatherURL = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(city)&APPID=8bad8461edbaf3ff50aa2f2fd8ad8a71&units=imperial")!
let weatherResource = Resource<WeatherViewModel>(url: weatherURL) { data in
let weatherVM = try? JSONDecoder().decode(WeatherViewModel.self, from: data)
return weatherVM
}
Webservice().load(resource: weatherResource) { result in
}
}
}
ViewModel
struct WeatherListViewModel {
private var weatherViewModels = [WeatherViewModel]()
}
struct WeatherViewModel: Decodable {
let name: String
let main: TemperatureViewModel
}
struct TemperatureViewModel: Decodable {
let temp: Double
let temp_min: Double
let temp_max: Double
}
Example of JSON data:
{
"coord":{
"lon":-0.13,
"lat":51.51
},
"weather":[
{
"id":300,
"main":"Drizzle",
"description":"light intensity drizzle","icon":"09d"
}
],
"base":"stations",
"main":{
"temp":280.32,
"pressure":1012,
"humidity":81,
"temp_min":279.15,
"temp_max":281.15
},
"visibility":10000,
"wind":{
"speed":4.1,
"deg":80
},
"clouds":{
"all":90
},
"dt":1485789600,
"sys":{
"type":1,
"id":5091,
"message":0.0103,
"country":"GB",
"sunrise":1485762037,
"sunset":1485794875
},
"id":2643743,
"name":"London",
"cod":200
}
By making the result of JSONDecoder().decode an optional (try?), you are ensuring that you get nil if the decoding goes wrong. You can catch decoding related issues quickly by implementing proper catch blocks. E.g.:
do {
let decoder = JSONDecoder()
let messages = try decoder.decode(WeatherViewModel.self, from: data)
print(messages as Any)
} catch DecodingError.dataCorrupted(let context) {
print(context)
} catch DecodingError.keyNotFound(let key, let context) {
print("Key '\(key)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
} catch DecodingError.valueNotFound(let value, let context) {
print("Value '\(value)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
} catch DecodingError.typeMismatch(let type, let context) {
print("Type '\(type)' mismatch:", context.debugDescription)
print("codingPath:", context.codingPath)
} catch {
print("error: ", error)
}
Not a direct answer to your question, but surely will reduce other's time to understand which part of decoding is going wrong.
Your WeatherViewModel property city is a String, but there is no "city" key in your JSON.
Why do we get nil value, when decoding the value?
Reasons:
The response parameter may be the first letter as capital.
Solution:
The coding keys concept is to out to nil value.
Example:
struct Example{
var a: string
var b: string
enum Codingkeys: String,CodingKey{
case a = "a"
case b = "b"
}
}

Swift: How to use response from SocketIO emitWithAck method

I'm using the SocketIO library to connect my iOS app to my server.
I want to emit some data to the server and get a json dictionary back in the acknowledgment. I currently have something like this:
SocketHandler.mySocket.emitWithAck("my_event", [session, someInput]).timingOut(after: 3) {data in
let myData = try? JSONDecoder().decode(myStruct.self, from: data)
MyStruct is defined as Class inheriting from Decodable and resembles the structure of the json I expect.
I get the following error: Cannot convert value of type 'Any' to expected argument type 'Data'
Any idea how I can tackle that type casting? Or would I need to go a totally other route?
(Swift 4.1 for iOS 11.3)
Cheers!
If anyone else is wondering how to use SocketIO with Decodable, I created a little extension for the client to accept Decodable in the callback, based on Dan Karbayev's answer.
import Foundation
import SocketIO
extension Decodable {
init(from any: Any) throws {
let data = try JSONSerialization.data(withJSONObject: any)
self = try JSONDecoder().decode(Self.self, from: data)
}
}
extension SocketIOClient {
func on<T: Decodable>(_ event: String, callback: #escaping (T)-> Void) {
self.on(event) { (data, _) in
guard !data.isEmpty else {
print("[SocketIO] \(event) data empty")
return
}
guard let decoded = try? T(from: data[0]) else {
print("[SocketIO] \(event) data \(data) cannot be decoded to \(T.self)")
return
}
callback(decoded)
}
}
}
Usage:
socket.on("location") { (data: LocationEventData) in
// ...
}
socket.on("success") { (name: String) in
// ...
}
Where LocationEventData and String are Decodable.
There're two things:
decode(_:from:) accepts a Data as a second parameter. To be able to decode from Any you'll need to add an extension to first serialize the data and then pass it to JSONDecoder, like this:
extension Decodable {
init(from any: Any) throws {
let data = try JSONSerialization.data(withJSONObject: any)
self = try JSONDecoder().decode(Self.self, from: data)
}
}
AckCallback's parameter is of an array type (i.e. [Any]), so you should get the first element of that array.
To make sure that you have indeed a decodable data (a dictionary or a JSON object) you can write something like this:
SocketHandler.mySocket.emitWithAck("my_event", [session, someInput]).timingOut(after: 3) { data in
guard let dict = data.first as? [String: Any] else { return }
let myData = try? myStruct(from: dict)
// ...
}

How to decode a codable property in two data types simply if one of them always is empty?

I receive from a post request, this JSON:
"clinic_info": {
"city": "Querétaro",
"state": "Querétaro",
"country": "México",
"phone": null,
"ext": null,
"coords": "20.6046089,-100.37826050000001",
"location": "Querétaro"
}
But when it is empty the JSON is:
"clinic_info": []
This produces an error: Expected to decode Dictionary but found an array instead.
This is happening because decoder want dictionary and your JSON is array
Need to check before decoding that JSON response is dictionary or Array and do decoding accordingly.
If you find Dictionary then do like this
let myData = try JSONDecoder().decode(YourModel.self, from: jsonData)
If you find Array then do like this
let myData = try JSONDecoder().decode([YourModel].self, from: jsonData)
You can do it using try, throw like that
import Foundation
struct ClinicData: Codable {
let clinicInfo: ClinicInfo?
enum CodingKeys: String, CodingKey {
case clinicInfo = "clinic_info"
}
}
struct ClinicInfo: Codable {
let city, state, country: String
let coords, location: String
}
// MARK: Convenience initializers
extension ClinicData {
init(data: Data) throws {
self = try JSONDecoder().decode(ClinicData.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
func jsonData() throws -> Data {
return try JSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
**get clinicInfo**
if let clinicData = try? ClinicData.init(data: Data()), let clinicInfo =
clinicData.clinicInfo{
}
The service that provides those JSON responses replies with:
"clinic_info": { ... }
Where ... is a valid JSON object.
But when it is empty, you are saying it looks like this:
"clinic_info": []
Notice the [] that say this is an empty array of objects.
You might want to change the service response (if possible), since it looks inconsistent to me having it return an object when it has valid data, and an array when there is no valid data.
The error message you are getting is clear:
Expected to decode Dictionary but found an array instead.
It expected an object {}, but found an array [].
The Array class has a method for this.
Using typeof will always return "object".
The code below shows how to use the isArray() method in the Array class.
const obj = {
_array: [],
_object: {}
}
console.log(Array.isArray(obj._array)); // true
console.log(Array.isArray(obj._object)); // false

How to iterate through JSON

I have a method which performs a GET request to an API:
public func getApiData(completion: #escaping () -> (), fullUrl: String)
{
let session = URLSession.shared
let url = URL(string: fullUrl)!
let task = session.dataTask(with: url) { (data, _, _) -> Void in
if let data = data {
self.serializeToJSON(jsonData: data)
completion()
}
}
task.resume()
}
Using SwiftyJSON I then convert the data into JSON:
private func serializeToJSON(jsonData: Data) {
self.json = JSON(data: jsonData)
print(self.json)
for (index,item) in self.json {
print("hi")
}
}
Printing the full JSON gives:
[{"TenantID":1,"Tenant1":"RAC"},{"TenantID":2,"Tenant1":"VictorMillwell"},{"TenantID":3,"Tenant1":"Comfort"},{"TenantID":4,"Tenant1":"Greenlight"}]
However the JSON can't be iterated through as the print("hi") isn't executed, I'm not sure why, I've looked everywhere on the internet to understand why it doesn't iterate and I cant seem to understand why.
Does anyone know why?
There is a good tutorial here, but in the manual it says you can loop like this:
// If json is .Dictionary
for (key,subJson):(String, JSON) in self.json {
// Do something you want
}
// If json is .Array
// The `index` is 0..<json.count's string value
for (index,subJson):(String, JSON) in self.json {
// Do something you want
}
if you don't know if it's a dictionary or array, maybe you can do it like this:
switch self.json.type {
case .array:
for (index,subJson):(String, JSON) in self.json {
// Do something you want
}
case .dictionary:
for (key,subJson):(String, JSON) in self.json {
// Do something you want
}
default:
// Do some error handling
}
It isn't clear why you want to enumerate the JSON. It's trivial to decode this JSON in Swift 4:
struct Tenant:Decodable { let TenantID:Int; let Tenant1:String }
let arr = try! JSONDecoder().decode([Tenant].self, from: data)
Now arr is a Swift array of Tenant, where each Tenant has a TenantID property and a Tenant1 property. And now you can do whatever you like with that array, including cycling through it if you wish.

How to convert a Swift object to a dictionary

I'm relatively new to iOS programming. However, I would have assumed that Swift would have an automated way of converting objects to JSON and vice versa. That being said, I have found several libraries that can do this.
HOWEVER...
It seems that no matter how you post data to a web service (even using something like AlamoFire), the requests must be a dictionary. All these forums show examples of how easy it is to convert the returned JSON string to objects. True. But the request needs to be manually coded. That is, go through all of the object properties and map them as a dictionary.
So my question is this: Am I missing something? Have I got this all wrong and there's a super-easy way to either (a) send JSON (instead of a dictionary) in the REQUEST or (b) convert an object automatically to a dictionary?
Again, I see how easy it is to deal with a JSON response. I'm just looking for an automatic way to convert the request object I want to post to a web service into a format that a library like AlamoFire (or whatever) requires. With other languages this is fairly trivial, so I'm hoping there's an equally easy and automated way with Swift.
I must disagree with #Darko.
In Swift 2,
use protocol oriented programming and the simple reflection offered by Mirror class :
protocol JSONAble {}
extension JSONAble {
func toDict() -> [String:Any] {
var dict = [String:Any]()
let otherSelf = Mirror(reflecting: self)
for child in otherSelf.children {
if let key = child.label {
dict[key] = child.value
}
}
return dict
}
}
then you can use this protocol with your request class and produce the desired dictionary :
class JsonRequest : JSONAble {
var param1 : String?
// ...
}
let request = JsonRequest()
// set params of the request
let dict = request.toDict()
// use your dict
My solution to this will be something like this:
extension Encodable {
var dict : [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] else { return nil }
return json
}
}
and usage will be something like this:
movies.compactMap { $0.dict }
Swift currently does not support advanced reflection like Java or C# so the answer is: no, there is not an equally easy and automated way with pure Swift.
[Update] Swift 4 has meanwhile the Codable protocol which allows serializing to/from JSON and PLIST.
typealias Codable = Decodable & Encodable
Without using reflection, and works for nested objects (Swift 4):
protocol Serializable {
var properties:Array<String> { get }
func valueForKey(key: String) -> Any?
func toDictionary() -> [String:Any]
}
extension Serializable {
func toDictionary() -> [String:Any] {
var dict:[String:Any] = [:]
for prop in self.properties {
if let val = self.valueForKey(key: prop) as? String {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Int {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Double {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Array<String> {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Serializable {
dict[prop] = val.toDictionary()
} else if let val = self.valueForKey(key: prop) as? Array<Serializable> {
var arr = Array<[String:Any]>()
for item in (val as Array<Serializable>) {
arr.append(item.toDictionary())
}
dict[prop] = arr
}
}
return dict
}
}
Just implement properties and valueForKey for the custom objects you want to convert. For example:
class Question {
let title:String
let answer:Int
init(title:String, answer:Int) {
self.title = title
self.answer = answer
}
}
extension Question : Serializable {
var properties: Array<String> {
return ["title", "answer"]
}
func valueForKey(key: String) -> Any? {
switch key {
case "title":
return title
case "answer":
return answer
default:
return nil
}
}
}
You can add more value types in the toDictionary function if you need.
The latest solution that I found after lots of digging throughout Stack Overflow is:
//This block of code used to convert object models to json string
let jsonData = try JSONEncoder().encode(requestData)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
//This method is used to convert jsonstring to dictionary [String:Any]
func jsonToDictionary(from text: String) -> [String: Any]? {
guard let data = text.data(using: .utf8) else { return nil }
let anyResult = try? JSONSerialization.jsonObject(with: data, options: [])
return anyResult as? [String: Any]
}
//Use above method something like this
let params = jsonToDictionary(from: jsonString) ?? [String : Any]()
//Use params to pass in paramters
Alamofire.request(completeUrl, method: .post, parameters: params, encoding:JSONEncoding.prettyPrinted, headers: myHeaders){
response in
//Do whatever you want with response of it.
}
Note:
I combine this solution from multiple answers.
This solution i used with alamofire because alamofire only accept parameter at this format "[String:Any]".
You can also use the ObjectMapper library. It has a "toJSON" method that converts your object to a dictionary.
in short
let dict = Mirror(reflecting: self).children.map({ $0 }).reduce(into: [:]) { $0[$1.label] = $1.value }
Example how to use Mirror with conversion to specific Dictionary type:
protocol DictionaryConvertible { }
extension DictionaryConvertible {
func toDictionary() -> [String: CustomStringConvertible] {
Dictionary(
uniqueKeysWithValues: Mirror(reflecting: self).children
.compactMap { child in
if let label = child.label,
let value = child.value as? CustomStringConvertible {
return (label, value)
} else {
return nil
}
}
)
}
}