Using JSONSerialization() to dynamically figure out boolean values
Asked Answered
C

3

12

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:

Credo answered 4/4, 2018 at 0:52 Comment(0)
Y
7

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
Yungyunick answered 4/4, 2018 at 1:24 Comment(5)
You can actually check for those internal types if you go to the CF level where they're public; see my answer.Amelina
@CharlesSrstka I didn't say you can't check for the internal types. I suggested that you shouldn't because, well, they are internal. Your check is fine. I mean not to check for __NSCFBoolean.Yungyunick
__NSCFBoolean is just a toll-free-bridge for CFBoolean, which we can check for easily enough.Amelina
thank you very much, I started to think a similar route because when doing a pretty-print of the dictionary, it was able to print the fact that they are boolean valuesCredo
I like the fact that you aren't "hard-coding" the actual type so this seems like the safest routeCredo
A
13

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")
    }
}
Amelina answered 4/4, 2018 at 1:23 Comment(1)
thank you very much, I started to think a similar route because when doing a pretty-print of the dictionary, it was able to print the fact that they are boolean valuesCredo
Y
7

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
Yungyunick answered 4/4, 2018 at 1:24 Comment(5)
You can actually check for those internal types if you go to the CF level where they're public; see my answer.Amelina
@CharlesSrstka I didn't say you can't check for the internal types. I suggested that you shouldn't because, well, they are internal. Your check is fine. I mean not to check for __NSCFBoolean.Yungyunick
__NSCFBoolean is just a toll-free-bridge for CFBoolean, which we can check for easily enough.Amelina
thank you very much, I started to think a similar route because when doing a pretty-print of the dictionary, it was able to print the fact that they are boolean valuesCredo
I like the fact that you aren't "hard-coding" the actual type so this seems like the safest routeCredo
C
5

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://mcmap.net/q/637628/-is-there-a-correct-way-to-determine-that-an-nsnumber-is-derived-from-a-bool-using-swift

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)

Credo answered 4/4, 2018 at 1:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.