I am getting a JSON string from the server (or file).
I want to parse that JSON string and dynamically figure out each of the value types.
However, when it comes to boolean values, JSONSerialization just converts the value to 0 or 1, and the code can't differentiate whether "0" is a Double, Int, or Bool.
I want to recognize whether the value is a Bool without explicitly knowing that a specific key corresponds to a Bool value. What am I doing wrong, or what could I do differently?
// What currently is happening:
let jsonString = "{\"boolean_key\" : true}"
let jsonData = jsonString.data(using: .utf8)!
let json = try! JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as! [String:Any]
json["boolean_key"] is Double // true
json["boolean_key"] is Int // true
json["boolean_key"] is Bool // true
// What I would like to happen is below (the issue doesn't happen if I don't use JSONSerialization):
let customJson: [String:Any] = [
"boolean_key" : true
]
customJson["boolean_key"] is Double // false
customJson["boolean_key"] is Int // false
customJson["boolean_key"] is Bool // true
Related:
How do I get NSJSONSerialization to output a boolean as true or false?
Is there a correct way to determine that an NSNumber is derived from a Bool using Swift?
This confusion is a result of the "feature" of all the wonderful magic built into the Swift<->Objective-C bridge. Specifically, the is and as keywords don't behave the way you'd expect, because the JSONSerialization object, being actually written in Objective-C, is storing these numbers not as Swift Ints, Doubles, or Bools, but instead as NSNumber objects, and the bridge just magically makes is and as convert NSNumbers to any Swift numeric types that they can be converted to. So that is why is gives you true for every NSNumber type.
Fortunately, we can get around this by casting the number value to NSNumber instead, thus avoiding the bridge. From there, we run into more bridging shenanigans, because NSNumber is toll-free bridged to CFBoolean for Booleans, and CFNumber for most other things. So if we jump through all the hoops to get down to the CF level, we can do things like:
if let num = json["boolean_key"] as? NSNumber {
switch CFGetTypeID(num as CFTypeRef) {
case CFBooleanGetTypeID():
print("Boolean")
case CFNumberGetTypeID():
switch CFNumberGetType(num as CFNumber) {
case .sInt8Type:
print("Int8")
case .sInt16Type:
print("Int16")
case .sInt32Type:
print("Int32")
case .sInt64Type:
print("Int64")
case .doubleType:
print("Double")
default:
print("some other num type")
}
default:
print("Something else")
}
}
When you use JSONSerialization, any Bool values (true or false) get converted to NSNumber instances which is why the use of is Double, is Int, and is Bool all return true since NSNumber can be converted to all of those types.
You also get an NSNumber instance for actual numbers in the JSON.
But the good news is that in reality, you actually get special internal subclasses of NSNumber. The boolean values actually give you __NSCFBoolean while actual numbers give you __NSCFNumber. Of course you don't actually want to check for those internal types.
Here is a fuller example showing the above plus a workable solution to check for an actual boolean versus a "normal" number.
let jsonString = "{\"boolean_key\" : true, \"int_key\" : 1}"
let jsonData = jsonString.data(using: .utf8)!
let json = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [String:Any]
print(type(of: json["boolean_key"]!)) // __NSCFBoolean
json["boolean_key"] is Double // true
json["boolean_key"] is Int // true
json["boolean_key"] is Bool // true
print(type(of: json["int_key"]!)) // __NSCFNumber
json["int_key"] is Double // true
json["int_key"] is Int // true
json["int_key"] is Bool // true
print(type(of: json["boolean_key"]!) == type(of: NSNumber(value: true))) // true
print(type(of: json["boolean_key"]!) == type(of: NSNumber(value: 1))) // false
print(type(of: json["int_key"]!) == type(of: NSNumber(value: 0))) // true
print(type(of: json["int_key"]!) == type(of: NSNumber(value: true))) // false
Because JSONSerialization converts each of the values to an NSNumber, this can be achieved by trying to figure out what each NSNumber instance is underneath: https://stackoverflow.com/a/30223989/826435
let jsonString = "{ \"boolean_key\" : true, \"integer_key\" : 1 }"
let jsonData = jsonString.data(using: .utf8)!
let json = try! JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as! [String:Any]
extension NSNumber {
var isBool: Bool {
return type(of: self) == type(of: NSNumber(booleanLiteral: true))
}
}
(json["boolean_key"] as! NSNumber).isBool // true
(json["integer_key"] as! NSNumber).isBool // false
(Note: I already got similar [better] answers as I was typing this up, but I figured to leave my answer for anyone else looking at different approaches)
Related
Converting to UInt64 always returns nil in Swift. My Dictionary looks like follows:
Dictionary:
let response = response as! Dictionary<String, Any>
print(response)
1 element
▿ 0 : 2 elements
- key : "userId"
- value : Optional(1301400295)
Swift Code:
guard let userId = response["userId"] as? UInt64 else { return }
let user = getUser(userId) // - (User* _Nullable) getUser:(uint64_t)eventId;
userId becomes always nil here.
There are lots of problems here. The key one is that an Int (which is likely close to what response["userId"] holds) is not a UInt64.
let x = 1
x as? UInt64 // nil
What you probably want is closer to this:
guard let userId = response["userId"] as? Int else { return }
let user = getUser(UInt64(userId))
Note that what you've shown here is not JSON. It looks like a [String:Any], and it looks like the actual value of response["userId"] is Int? rather than Int. That's getting really complicated. I would look at whatever code generated this [String:Any] and replace it with something that generates a struct (such as JSONDecoder). That's going to simplify the rest of the program substantially.
so I have a json object that looks like this.
{
geometry = {
location = {
lat = "51.5194133";
lng = "-0.1269566";
};
};
id = ad6aaec7b7b0fa2c97a127c24845d76135e760ae;
"place_id" = ChIJB9OTMDIbdkgRp0JWbQGZsS8;
reference = "CmRRAAAAiC-ErdlAvz74Drejj2mAAh6Plr46e889a3Uv6CrRXFqNtVatoFsOTarDH0KU8KCkWoN--QGv01RSjLBZblbrAHNPGDVdiXikedid0vKMVM_LQtXstrSQFt4s-Z-Wi-1AEhDJRWc9bdWpKHPPOrt7QGTqGhSJgMPENn_wSGbprGYLv52csv5BtQ";
}
I was wondering how you can extract the information at different levels for example the location object is an object within the geometry objet and I want to extract lat from there how can I do this?
I can print out the location object like:
let setOne = jsonResult["results"]! as! NSArray
let y = setOne[0] as? [String: AnyObject]
print(y!)
print((y!["geometry"]!["location"]!)!["lat"])
but when I try to do:
print((y!["geometry"]!["location"]!)!["lat"])
it gives me the error: Type 'Any' has no subscript members
Perhaps the easiest way to do this is to use JSONDecoder to decode your JSON directly into structs.
You first need to define structs that match your JSON structure, like so:
struct Place: Codable {
let geometry: Geometry
let id: String
let place_id: String
let reference: String
}
struct Geometry: Codable {
let location: Location
}
struct Location: Codable {
let lat: String
let lng: String
}
Now, assuming that jsonResult["results"] is in fact an NSArray, you first need to convert it to Data and then use the JSONDecoder to decode the JSON into the structs:
if let data = try? JSONSerialization.data(withJSONObject: jsonResult["results"], options: []) {
if let places = try? JSONDecoder().decode(Array<Place>.self, from: data) {
print(places[0].geometry.location.lat) //prints "51.5194133"
}
}
The advantage of this approach is that you write much less code to do the actual decoding.
Note that if any of the JSON elements might be missing, you should declare the corresponding struct let property as an optional. For example, if reference might not always be present, you would code it as:
let reference: String?
Anything that is not optional must be present in the JSON, or the decode will fail. This is why you want to use try? when decoding, so that your app does not crash.
Lets say you have an array of results data, you don't need to use NSArray since you are using Swift so here you can start.
In Swift you need to specify the type, And also try not to use AnyObject unless you really need it, Always provide the type and thats what the error is saying:
// make sure that results exist
if let resultArray = jsonResult["results"] as? [Dictionary<String,Any>] {
// loop through the result array
for object in resultArray {
// get geometry from the object in the array
if let geometry = object["geometry"] as? Dictionary<String,Any>{
// make sure it has location
if let location = geometry["location"] as? Dictionary<String,Any>{
// now get the lat or lng safely.
let lat = location["lat"] as? String ?? "0.0"
let lng = location["lng"] as? String ?? "0.0"
}
}
}
}
Note: Never use force-unwrap ! because your app will crash if something went wrong or that specific object is not found, make sure to use guard let or if let at least, although this is manual parsing you can look into Swift 4 new Encoding, Decoding and Serialization in Swift 4.
Considering you have an array of data as results (jsonResult), start accessing your array and identify the type of each parameter.
if let resultArray = jsonResult["results"] as? NSArray {
for aResult in resultArray {
//using "if let" simply means, in this case, if "geometry" is indeed a dictionary it will execute the statement/s inside this "if" block.
if let geometry = aResult["geometry"] as? NSDictionary {
if let location = geometry["location"] as? NSDictionary {
if let latValue = location["lat"] as? String {
//This will only be executed if the value is String
print(latValue)
}
if let lngValue = location["lng"] as? String {
print(lngValue)
}
}
}
}
}
I'm having a dictionary [String: Any!] that has values like integers, floats and strings. When I'm creating URLComponents using below code, its not taking values related to integers and floats.
func queryItems(dictionary: [String: Any]) -> [URLQueryItem] {
return dictionary.map {
URLQueryItem(name: $0, value: $1 as? String)
}
}
I think you should consider using [String:String] instead of [String:Any] and convert your values to String one step before sending it to queryItems function.But if you want to leave it like this than casting from value Int,Float,Double with as? String always fails.
you can use String().
Obviously Any could be anything. But if you have some control of your inputs one easy option is casting to LosslessStringConvertible. String, Substring and your basic numeric types all conform to it.
Warnings:
Bool returns true or false.
NSObject does not conform to this, so if you're mixing in NSNumber or NSString you need to account for those also.
func queryItems(dictionary: [String: Any]) -> [URLQueryItem] {
dictionary.map {
URLQueryItem(name: $0, value: ($1 as? LosslessStringConvertible)?.description)
}
}
I am using this line to get data
let productDict = arrProductCart[sender.tag] as! [String: AnyObject]
and I want to filter data from dictionary so i am using this code
let filteredSubitems = productDict.filter{
$0["groupid"] as!String != "1"
}
it is giving me error Type '(String, AnyObject)' has no subscript members
do i need to convert [String: AnyObject] to [String: String]?
what to do.
Most probably you want to filter arrProductCard array instead of productDict, which doesn't make sense. Try this:
let filteredProducts = arrProductCard.filter{
guard let groupId = $0["groupid"] as? String else { return false }
return groupId != "1"
}
You should avoid forced unwrapping whenever you can. Note that your code inside filter closure will crash if there is no groupid value in the dictionary or if it is not a string.
Edit:
If you're using NSMutableArray for some reason, you can just filter it with a predicate:
let mutableArray = NSMutableArray(array: [["groupid": "1"], ["groupid": "2"]])
let groupIdPredicate = NSPredicate(format: "groupid != %#", "1")
mutableArray.filter(using: groupIdPredicate)
However, I would strongly recommend to use Swift native collections.
I apologise for the title of this question. I have no idea what else to call it.
So... When calling the following:
let testData: [NSObject : AnyObject] = getTestData()
print(testData)
I get this output:
[data: {"TypeId":7,"DataList":null,"TypeName":"This is a test"}]
How would I be able to access the value 7 for the key "TypeId"?
EDIT:
Please note that it's holding { } brackets, not only [ ], thus a cast to NSDictionary is not possible as far as I have tried.
Kind regards,
Anders
You can achieve plist-like nested structures using Any type for dictionary values which is Swift's somewhat counterpart to Objective-C's id type but can also hold value types.
var response = Dictionary()
response["user"] = ["Login": "Power Ranger", "Password": "Mighty Morfin'"]
response["status"] = 200
Any seems to be better than AnyObject because in the above code response["status"] is of type Swift.Int, while using value type of AnyObject it is __NSCFNumber.
The way most people do it is to parse annoying JSON data as custom objects. That should be done as soon as you get the JSON. Ideally, data as JSON should not be used outside your communication code, example:
First, let's define a class to hold your server data:
class MyServerObject {
let typeId: Int
let typeName: String
let dataList: [AnyObject]?
init(dictionary: Dictionary<String, AnyObject>) {
let dataDictionary = dictionary["data"] as! Dictionary<String, AnyObject>
self.typeId = dataDictionary["TypeId"] as! Int
self.typeName = dataDictionary["TypeName"] as! String
self.dataList = dataDictionary["DataList"] as? [AnyObject]
}
}
Note that init method is already parsing the JSON. This doesn't have to be done in init, you could also create a static parse method that will return a new instance.
Usage:
// demo data
let jsonString = "{\"data\": {\"TypeId\":7,\"DataList\":null,\"TypeName\":\"This is a test\"}}"
let jsonData = jsonString.dataUsingEncoding(NSUTF8StringEncoding)!
let json = try! NSJSONSerialization.JSONObjectWithData(jsonData, options: [])
// parsing
let myServerObject = MyServerObject(dictionary: json as! Dictionary<String, AnyObject>)
// now we can simply read data as properties
print(myServerObject.typeId)
print(myServerObject.typeName)
One of the good thing about this solution is that we can check the JSON format and all the properties are parsed with the correct types.
Parsing can be hierarchical, for example, if your dataList contains complex objects, let's call them DataListItem, your parsing method can parse each item separately and put them into a [DataListItem], e.g.
if let dataListJSON = dataDictionary["DataList"] as? [Dictionary<String, AnyObject>] {
self.dataList = dataListJSON.map({ DataListItem($0) })
}
Also note that when parsing as! will crash the app when the format is invalid. as? will return nil if the types don't match. as? is very useful for types that can be nil because they are parsed as NSNull instances.
taking in account your data ...
print(testData)
/*
[data: {
DataList = null;
TypeId = 7;
TypeName = "This is a test";
}]
*/
// DataList type should be declared somewhere
class DataList {}
// parse data or set default value, if 'key' doesn't exist
if let data = testData["data"] as? [String:AnyObject] {
let dataList = data["DataList"] as? DataList // nil
let typeId = data["TypeId"] as? Int ?? 0 // 7
let typeName = data["TypeName"] as? String ?? "" // This is test
}