kgaidis
kgaidis

Reputation: 15579

Using JSONSerialization() to dynamically figure out boolean values

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

Answers (3)

kgaidis
kgaidis

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

rmaddy
rmaddy

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

Charles Srstka
Charles Srstka

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 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")
    }
}

Upvotes: 13

Related Questions