Conditionally decoding JSON based on a field in the JSON - json

I am receiving JSON from an API and the response can be one of 30 types. Each type has a unique set of fields, but all responses have a field type which states which type it is.
My approach is to use serde. I create a struct for each response type and make them decodable. Once I have that how do I choose which struct should be used for a freshly received message?
At the moment, I've created another struct TypeStruct with only a single field for type. I decode the JSON into a TypeStruct, then choose the appropriate struct for received message, based on type value, and decode the message again.
I would like to get rid of this decoding duplication.

You can use the existing enum deserialization. I'll give a step by step example to deserialize your format to the following enum:
#[derive(Debug, PartialEq, Eq, Deserialize)]
enum MyType {
A {gar: ()},
B {test: i32},
C {blub: String},
}
Start with an example json string:
let json = r#"{"type": "B", "test": 42}"#;
Turn it into a Value enum
let mut json: serde_json::Value = serde_json::from_str(json).unwrap();
Rip out the type field
let type_ = {
let obj = json.as_object_mut().expect("object");
let type_ = obj.remove("type").expect("`type` field");
if let serde_json::Value::String(s) = type_ {
s
} else {
panic!("type field not a string");
}
};
Create the "proper" enum json. A struct with a single field where the name of the field is the enum variant and the value of the field is the variant value
let mut enum_obj = std::collections::BTreeMap::new();
enum_obj.insert(type_, json);
let json = serde_json::Value::Object(enum_obj);
Use the generated json deserializer to turn the json into a value of your enum
let obj: MyType = serde_json::from_value(json).unwrap();

Related

Empty dictionary are encoded to empty JSON arrays if key is UUID

I'm trying to encode/decode JSON with Swift's JSONEncoder/Decoder. My JSON contains a dictionary with UUID as Key. If this dictionary is empty, Swift fails with
Expected to decode Array<Any> but found a dictionary instead.
While analyzing I noticed, that Swift creates different repressions of the empty dictionary depending on the key's type. The following minimum example illustrates the problem quite well:
import Foundation
typealias Key = UUID // or String
struct Foo: Codable {
let data: [Key: String]
}
let foo = Foo(data: [:])
let encodedData = try JSONEncoder().encode(foo)
let foo2 = try JSONDecoder().decode(Foo.self, from: encodedData)
print(String(decoding: encodedData, as: UTF8.self))
When using UUID as key's type you get:
{"data":[]}
and when using String as key's type you get:
{"data":{}}
I'd expect {"data":{}} in all cases. What I'm doing wrong here?

How can I decode a generic JSON response in Swift 4?

I'm building an app with Swift 4 that consumes a JSON-RPC API. The responses all have the same general format:
{
"jsonrpc": "2.0",
"result" : { "data_type" : [ ...a bunch of instances of data_type... ] }
"id": 1
}
Where data_type would be payments, channels, peers, and so on depending on the query.
I have Decodable struct definitions for each of the data types, but I don't know how to handle the main response.
I really don't care about the jsonrpc or id fields, I'm just interested in the contents of result.
I tried:
struct LightningRPCResponse: Decodable {
let id: Int
let result: String
let json_rpc: String
}
But I got the error:
Expected to decode String but found a dictionary instead
So I tried:
struct LightningRPCResponse: Decodable {
let id: Int
let result: Dictionary
let json_rpc: String
}
But I got the error:
Reference to generic type 'Dictionary' requires arguments in <...>
Is what I'm trying to do possible or do I need to create separate response decoders to correspond to every single RPC request?
Or...should I just use string manipulation to lop off the superfluous data?
You could make two structs:
struct generalStruct:Codable {
let jsonrpc:String
let id:Int
let result:[resultsStruct]
}
struct resultsStruct{
//assuming that you have strings in here, cause you didn't specify that. And it's considered as a Dictionary like: "data_tupe":"string_value" or if you have an array also here than just make another struct or just make data_type:[String]
let data_type:String
}
With that structs you can decode now. Example:
let json = try decoder.decode(generalStruct.self, from: response.data!)
//here you can get access to each element of your 'data_type'
for obj in json.result{
for data in obj.data_type {
//you have every element from dict access here if its more objects inside every 'data_type'
}
}

JSON's numeric value to string in swift?

I get json from server and I need to convert id field to string in swift code.
The problem is json sometimes returns "12345", sometimes returns 12345 (with or without quotes).
Is it possible to resolve this issue without of checking the value type and checking if the conversion result is nil?
UPDATED
Example of code I use with checking conversion result:
let result = (some_index as? String) ?? String(some_index as! Int)
The problem is in objective-C you have [NSString stringWithFormat:#"%#", some_object]. But in swift you have optionals and it tries to insert word "optional" into result.
UPDATED
STOP spam with random answers about optionals. The question is concrete - "how to simply unwrap json value which may look like String, Int or doesn't exist at all?"
swift How to remove optional String Character
In this question they ask how to convert Int? -> Int, String? -> String and similar. In my case I don't know if I have Int? or String? as the initial type.
Both Stringand Int conform to CustomStringConvertible, so you could optional downcast the value to CustomStringConvertible and use String Interpolation
let dict : [String:Any] = ["Foo" : 12345]
if let value = dict["Foo"] as? CustomStringConvertible {
let result = "\(value)"
}
And blame the owner of the web service for sending inconsistent data ;-)

How to use SwiftyJSON to read values from JSON with multiple objects and arrays

I have this JSON, I want to access the values using the SwiftyJSON library:
{"user":[{"id":"33","id_number":"0","first_name":"tom","last_name":"lily","city":"jeddah","gender":"0","DOB":"0000-00-00","phone_namber":"0000000000","email":"000"},
{"id":"34","id_number":"0","first_name":"tom","last_name":"lily","city":"jeddah","gender":"0","DOB":"0000-00-00","phone_namber":"0000000000","email":"000"}]}
This JSON contains arrays and objects. When I try this, it doesn't work:
JSON["lawyers"]["id"].intValue
How can I access the id and other values in this JSON?
Firstly, there's no "lawyers" in this JSON, and it'll never start parsing the data. Secondly, all of the values are String types, so if you want to use them as Int, you have to convert them.
So, you have a "user" array, which means that you have to iterate through that array.
After that, you will be able to work with the items in the "user" array and access its values.
Here's a function that I use. Its input is the JSON that I'm working with and it stores it in a dictionary.
var dataArray = [[String: String]]() // Init dictionary
func parseJSON(json: JSON) {
for item in json["user"].arrayValue { // This is the JSON array which contains the values that you need
let id = item["id"].stringValue // You access the value here
let first_name = item["first_name"].stringValue
let last_name = item["last_name"].stringValue
let email = item["email"].stringValue
let obj = ["id": id, "first_name": first_name, "last_name": last_name, "email": email]
dataArray.append(obj) // This appends the JSON parsed data to an array of dictionaries
}
}
Usage of this:
func usingTheParsedJSON(){
for user in dataArray {
print("user's id: ", user["id"], ", last_name: ", user["last_name"])
let convertedId: Int = Int(user["id"]) // If you want to use the id as Int. With this dictionary, you can only store it as String, since everything has to have the same type
}
}
If you can edit the JSON data, then with removing the quotation marks, you can use the numbers as Integers when parsing JSON.
let id = item["id"].intValue // This goes into the func's for loop
Note: to store this in the dictionary, you'll have to convert it to String with String(id). This method for storing data is not the best, I use this because I usually have strings and only one integer.
I hope this solves your problem. Let me know if you need anything else!
PS: there's a typo in the JSON data: phone_namber.

How do you create a SwiftyJSON dictionary out of optional AnyObjects?

I recently ran into a very time-consuming issue for a simple task of creating a JSON dictionary with optional objects. I'm using SwiftyJSON.
If I create the following dictionary, I get thrown errors
JSON type doesn't have an initializer that supports Dictionary<String,AnyObject?>
But if I simply change AnyObject to an non-optional type, it works.
var dict: Dictionary <String, AnyObject?> = [
"title" : title as? AnyObject,
"date" : date as? AnyObject,
]
var jsonData: JSON = JSON(dict) // this is where I get the error
I need to actually a JSON data set that potentially has nil values, but it seems like SwiftyJSON doesn't allow it.
Is there any way I can create a JSON dictionary with optional objects using SwiftyJSON?
Neither any key nor any value of a Swift Dictionary can be nil.
You could declare the variable as optional
var dict: Dictionary <String, AnyObject>?