How to return native type via `type(of:)` from JSON string

I'm trying to print a data type for a JSON string, but I get non-native types:


func printJSONTypes(jsonString: String) {
  // First, try to parse the JSON string into an object
  guard let jsonObject = try? JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!) else {
    print("Error: Unable to parse JSON string")
    return
  }

  // If the JSON object is a dictionary, iterate over its keys and values
  if let jsonDictionary = jsonObject as? [String: Any] {
    for (key, value) in jsonDictionary {
      print("\(key): \(type(of: value))")
    }
  }
}

let jsonString = """
{
  "name": "John",
  "age": 30,
  "isEmployed": true
}
"""

printJSONTypes(jsonString: jsonString)

// Output:
// name: NSTaggedPointerString
// age: __NSCFNumber
// isEmployed: __NSCFBoolean

Here I expect to get a native data type, but the type(of:) function returns runtime type.

I tried just comparing each data type switch case is String, but this way seems a bit dumb to me, because I would have to go through all the possible cases.
Mirror also outputs non-native types.

Any ideas?

JSONSerialization (see Apple Developer Documentation) really is an Obj-C object (NSJSONSerialization) and so are all of its JSON object types:

The class names are various implementation-level details of those Obj-C classes, which don't translate "prettily" into Swift types.

My question here is what are you trying to do with the types? Yes, you might want to display them, where "prettiness" is significant, but just about anything else you want to do will involve testing the type against one or more expected Swift types, for which you're going to extract a value that you can test with is or as?.

Just trying to create a utility that generates a data model from a JSON response, so I need native data types.

Of which there is only a handful: Bool, Double, String, Array and Dictionary. Also Int should you wish to distinguish json ints from doubles. You'll need to decide what type to assign to nil if that can be encountered in your json files.

If I rewrite the function like this:


func matchTypes(_ jsonString: String) -> String {
    guard let jsonObject = try? JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!) else { return String() }
    guard let jsonDictionary = jsonObject as? [String: Any] else { return String() }
    
    var result = ""
    
    for (key, value) in jsonDictionary {
        result += "\tlet \(key): "
        switch value {
        case is Int: result += "Int\n"
        case is String: result += "String\n"
        case is Double: result += "Double\n"
        case is Bool: result += "Bool\n"
        // 'Array' requires arguments in <...>
        // case is Array: result += "Array\n"
        // 'Dictionary' requires arguments in <...> | <Key: Hashable, Any>
        // case is Dictionary: result += "Dictionary\n"
        default: result += "\(type(of: value))\n"
        }
    }
    return result
}

let jsonString = """
{
  "name": "John",
  "age": 30,
  "isEmployed": true
}
"""

print(matchTypes(jsonString))

//    let age: Int
//    let name: String
//    let isEmployed: Int

//  isEmployed is Bool, not Int 🥲

then it turns out that I need to check every case

case is Array<Int>
case is Array<String>
case is Array<Float>
// etc

case is Dictionary<String, String>
case is Dictionary<String, Int>
// etc

You can of course just specify Array<Any>, but that's not quite right.

The result of JSONSerialization.jsonObject represents booleans and numbers all as the class cluster type NSNumber.

As you see, NSNumber quickly loses the underlying type information when used in further arithmetic or casted to more native Swift types, and happily treats its encoded true as the number one if asked for an integer.

But you can check the boolean values first in the way explained here: JSONSerialization turns Bool value to NSNumber - #3 by eskimo

1 Like

Ah, no, don't do that, as you'd also need to match [[String]], [[[String]]], [[[[String]]]] ad infinitum.

Yes, this is correct approach: [Any] or [Any?]. Once you have the array or dictionary match - recurse:

func match(_ value: Any?) {
    guard let value = value else {
        print("nil")
        return
    }
    switch value {
    case let v as Bool:
        print("Bool \(v)")
    case let v as Int:
        print("Int \(v)")
    case let v as Double:
        print("Double \(v)")
    case let v as String:
        print("String \(v)")
    case let elements as [Any?]:
        print("Array with \(elements.count) values")
        for (index, element) in elements.enumerated() {
            print("  element[\(index): ", terminator: "")
            match(element)
        }
    case let elements as [String: Any?]:
        print("Dictionary with \(elements.count) values")
        for (key, value) in elements {
            print("  key[\(key)]: ", terminator: "")
            match(value)
        }
    default:
        fatalError("TODO")
    }
}
2 Likes

JSON is untyped, so eagarly narrowing JSON numerics to integers is usually a bad idea, because an array of doubles like [1, 1.5, 2] could become heterogenous when imported into swift.

2 Likes