Reputation: 15579
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:
Upvotes: 12
Views: 3625
Reputation: 15579
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)
Upvotes: 5
Reputation: 318794
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
Upvotes: 7
Reputation: 17040
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 Int
s, Double
s, or Bool
s, but instead as NSNumber
objects, and the bridge just magically makes is
and as
convert NSNumber
s 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")
}
}
Upvotes: 13