Working with JSON in Swift


Show me a developer who has never had to work with JSON . Due to its simplicity, flexibility and visibility, this data presentation format has gained great popularity and is now used everywhere. So during the experiments with Swift, I quickly came across the need to parse JSON data.

Actually, the standard Foundation API - NSJSONSerialization - perfectly copes with the task of direct and inverse JSON conversion from a text representation to an object model . Apple has done some serious work to ensure that Swift and Objective-C code communicate directly and backward ( Using Swift with Cocoa and Objective-C), therefore, the use of the familiar Cocoa API is not only possible in practice, but also convenient and does not look unnatural:

let jsonString = "{\"name\":\"John\",\"age\":32,\"phoneNumbers\":[{\"type\":\"home\",\"number\":\"212 555-1234\"}]}"
let jsonData = jsonString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
let jsonObject: AnyObject! = NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions(0), error: nil)

But working with the result in a statically typed Swift is inconvenient - you need a chain of checks and type conversions to get any value. Next, I will consider solutions to this problem, and at the same time we will get acquainted with some features of Swift.

Formally obtained through NSJSONSerialization, the JSON representation consists of instances of Foundation types - NSNull, NSNumber, NSString, NSArray, NSDictionary. But runtime bridging provides full compatibility and interchangeability of these types and the corresponding Swift primitives - numeric types (Int, Double, etc.), String, Array, Dictionary. Therefore, in the Swift code, we can work with the resulting object “natively”. Suppose we need to check the value of the first phone number. On Objective-C, this might look like this:

NSString *number = jsonObject[@“phoneNumbers”][0][@“number”];
NSAssert(["212 555-1234" isEqualToString:number], @”numbers should match”);

By using dynamic typing, navigating the hierarchy of a JSON object does not cause problems in Objective-C. Swift, by contrast, uses strong static typing, so you must use explicit type casting at each step “deeper” in the hierarchy:

let person = jsonObject! as Dictionary
let phoneNumbers = person["phoneNumbers"] as Array
let phoneNumber = phoneNumbers[0] as Dictionary
let number = phoneNumber["number"] as String
assert(number == "212 555-1234")

Unfortunately, the current version of the compiler (Xcode 6 beta 2) generates an error for this code - there is a problem with parsing expressions of explicit type conversions with operands using subscripts . This can be circumvented through intermediate variables:

let person = jsonObject! as Dictionary
let phoneNumbers : AnyObject? = person["phoneNumbers"]
let phoneNumbersArray = phoneNumbers as Array
let phoneNumber : AnyObject? = phoneNumbersArray[0]
let phoneNumberDict = phoneNumber as Dictionary
let number : AnyObject? = phoneNumberDict["number"]
let numberString = number as String
assert(numberString == "212 555-1234")

This option works correctly, but it certainly looks awful. Combine getting the value in one expression with optional downcasting and optional chaining :

let maybeNumber = (((jsonObject as? NSDictionary)?["phoneNumbers"] as? NSArray)?[0] as? NSDictionary)?["number"] as? NSString
assert(maybeNumber == "212 555-1234")

Already better, but of course it is difficult to call such a record convenient - for reading and especially for editing.

It is clear that this is only a special case of a common problem - the complexity of working with dynamic data structures in strongly typed languages. There are different approaches to solving this problem, some change the way the data is processed. But for a specific case with parsing JSON objects, I wanted to find a simple solution that meets the following requirements:
  • compactness and readability of the code;
  • minimize the need for explicit typecasting;
  • the possibility and convenience of “disclosing” several levels of the hierarchy in one expression in a chain;
  • exclude the possibility of runtime errors due to a mismatch of the JSON structure with expectations - in such cases, the value of the entire expression should be nil;
  • Minimize CPU and memory overhead.

First of all, I tried to find and analyze ready-made solutions.

Enum representation

Several authors immediately suggest a similar approach:

Let us briefly consider their device using json-swift as an example:
  1. A new enum type ( github ) is introduced to represent JSON objects
  2. Using the associated values mechanism, the possible values ​​of this enum are set to primitive Swift types corresponding to JSON types ( github )
  3. The main type constructor checks the type of the passed object and returns an instance with the desired enum value ( github )
  4. At the same time, for containers (arrays and dictionaries), elements are processed recursively ( github )
  5. To navigate the JSON hierarchy, subscripts are implemented that return the appropriate elements for arrays and dictionaries ( github )
  6. For the inverse conversion from the JSON enumeration to the corresponding primitive Swift type, computed properties are used , which return the associated value only if the type matches ( github )

Transforming the original object model into such a representation, we get a convenient interface that can be used to navigate the hierarchy and get the value of the expected type:

let number = JSON(jsonObject)?[“phoneNumbers”]?[0]?[“number”]?.string
assert(number == "212 555-1234")

Here, the optional chaining mechanism provides a guarantee of the absence of runtime errors. When parsing an object with an inappropriate structure, the value of the entire expression will be nil (except for the case when the index is accessed outside the array).

It turns out that such a solution meets all the requirements put forward, except for one. When using it, a mandatory recursive traversal of the entire hierarchy of the JSON object occurs and a new object representation of the data is created. Of course, in some cases, such overheads do not play a fundamental role. But nevertheless, the whole solution cannot be called optimal in terms of using CPU and memory resources.

A cardinal way to solve the problem would be to use such an object JSON representation right at the stage of conversion from a text representation. But such an approach is already beyond the scope of the problem under consideration - convenient work with the native object representation of JSON.

Lazy Processing

Another approach to solving the complete conversion problem is to use “lazy" logic to check and type cast when traversing JSON. Instead of immediately re-creating the entire JSON hierarchy with the values ​​of the required types, you can do this at each step “in-depth” - for only one requested element. The implementation of just such an approach is offered by the notorious Mike Ash: gist.github.com/mikeash/f443a8492d78e1d4dd10

Unfortunately, with this approach, it will not be possible to present a separate JSON value in the same convenient form (enum + associated values). But such a solution is obviously more optimal. At first glance, there is also a small overhead in the form of creating an additional wrapper object at each step deep into the hierarchy. But these objects are defined as structures (struct Value), so their initialization and use can be well optimized by the compiler in Swift.

Decision

I still wanted to find a solution that does not use new types, but extends the behavior of standard types as necessary. Let's take a closer look at the expression with standard syntax

(((jsonObject as? NSDictionary)?["phoneNumbers"] as? NSArray)?[0] as? NSDictionary)?["number"] as? NSString

In fact, only transitions to elements of dictionaries and arrays cause problems here. This is because the subscript ( [1]or [“number”]) call imposes a requirement on the type of value to which it is applied — in our case, we are leading to an NSDictionary or to an NSArray. Or, on the other hand, the obtained values ​​from NSArray and NSDictionary are of type AnyObject, which requires a type cast to be used later in the call chain.

It turns out that the need for a cast will disappear if we operate on a universal type that initially supports both subscript options and returns objects of the same type. In Swift, the protocol formally corresponds to this definition:

protocol JSONValue {
    subscript(key: String) -> JSONValue? { get }
    subscript(index: Int) -> JSONValue? { get }
}

Thus, the protocol will determine the JSON value, which can always be accessed by subscript (with an Int or String parameter). As a result, you can get either an element of the collection (if the object is a collection, its type corresponds to the type subscript and an element with such subscript is in the collection), or nil.

To work with standard types in this way, you need to ensure that they match JSONValue. Swift allows you to add protocol implementations through extensions . As a result, the whole solution looks like this:

protocol JSONValue {
    subscript(#key: String) -> JSONValue? { get }
    subscript(#index: Int) -> JSONValue? { get }
}
extension NSNull : JSONValue {
    subscript(#key: String) -> JSONValue? { return nil }
    subscript(#index: Int) -> JSONValue? { return nil }
}
extension NSNumber : JSONValue {
    subscript(#key: String) -> JSONValue? { return nil }
    subscript(#index: Int) -> JSONValue? { return nil }
}
extension NSString : JSONValue {
    subscript(#key: String) -> JSONValue? { return nil }
    subscript(#index: Int) -> JSONValue? { return nil }
}
extension NSArray : JSONValue {
    subscript(#key: String) -> JSONValue? { return nil }
    subscript(#index: Int) -> JSONValue? { return index < count && index >= 0 ? JSON(self[index]) : nil }
}
extension NSDictionary : JSONValue {
    subscript(#key: String) -> JSONValue? { return JSON(self[key]) }
    subscript(#index: Int) -> JSONValue? { return nil }
}
func JSON(object: AnyObject?) -> JSONValue? {
    if let some : AnyObject = object {
        switch some {
        case let null as NSNull: return null
        case let number as NSNumber: return number
        case let string as NSString: return string
        case let array as NSArray: return array
        case let dict as NSDictionary: return dict
        default: return nil
        }
    } else {
        return nil
    }
}

A few notes:
  • to avoid conflict with standard subscripts in NSDictionary and NSArray, subscripts with named parameters are used - #key, #index;
  • a helper function is used to cast arbitrary values ​​to the JSONValue type, because the standard checking and casting operators work only for protocols marked with an attribute@objc (this is most likely due to the fact that correspondence to non- @objcprotocols can be added to such Swift types, data about which are not available in runtime);
  • although the code operates on Foundation types, runtime bridging ensures that Swift primitives work correctly.

As a result, we can use the expression to work with JSON:

let maybeNumber = JSON(jsonObject)?[key:"phoneNumbers"]?[index:0]?[key:"number"] as? NSString
assert(maybeNumber == "212 555-1234")

Although this is not the most compact version of those considered, such a solution fully meets all of the above requirements.

Alternative option

Based on the same idea, a variant using a protocol with an @objcattribute is possible . This allows you to use explicit type casting instead of an auxiliary function, but prohibits the use of subscripts - you will have to use ordinary methods instead. But these methods can be declared as @optional:

@objc
protocol JSON {
    @optional func array(index: Int) -> JSON?
    @optional func object(key: String) -> JSON?
}
extension NSArray : JSON {
    func array(index: Int) -> JSON? { return index < count && index >= 0 ? self[index] as? JSON : nil }
}
extension NSDictionary: JSON {
    func object(key: String) -> JSON? { return self[key] as? JSON }
}
extension NSNull : JSON {}
extension NSNumber : JSON {}
extension NSString : JSON {}

Usage example:

let maybeNumber = (jsonObject as? JSON)?.object?(“phoneNumbers”)?.array?(0)?.object?(“number”) as? NSString
assert(maybeNumber == "212 555-1234")

Not as compact as the option with subscripts. Someone may be confused by the number of question marks, but on the other hand, each of its use is understandable and carries a semantic load.

In my opinion, the solutions found meet the given requirements and look preferable to the other options considered. And the idea used - the allocation of a universal protocol with methods that return optional values ​​- can be used to conveniently work not only with JSON, but also with other dynamic data structures.

Code and usage examples are available on github .

Also popular now: