📜 ⬆️ ⬇️

Working with JSON in Swift


Show me a developer who has never had to work with JSON . Due to its simplicity, flexibility and clarity, this data presentation format has gained great popularity and is now used everywhere. Here I am, while experimenting with Swift, I quickly encountered the need to parse JSON data.

Actually, the standard Foundation API — NSJSONSerialization — does an excellent job with the task of direct and inverse JSON conversion from a textual representation into an object model. Apple has done a serious job to provide direct and reverse interaction between Swift and Objective-C code ( Using Swift with Cocoa and Objective-C ), so using the usual 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 - a chain of checks and type conversions is required 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 the NSJSONSerialization 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 received object “natively”. Suppose we need to check the value of the first phone number. In Objective-C, it could 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 causes no problems in Objective-C. Swift, on the other hand, uses strong static typing, so at each step “in depth” of the hierarchy, you need to use explicit type casts:

 let person = jsonObject! as Dictionary<String,AnyObject> let phoneNumbers = person["phoneNumbers"] as Array<AnyObject> let phoneNumber = phoneNumbers[0] as Dictionary<String,AnyObject> 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 explicit casting expressions with operands using subscripts . This can be bypassed through intermediate variables:

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

This option works correctly, but it looks awful of course. You can combine getting values ​​into one expression using 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 such a record is certainly difficult to call 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 languages ​​with strong typing. There are different approaches to solving this problem, some change the way data is processed. But for the specific case of parsing JSON objects, I wanted to find a simple solution that meets the following requirements:

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

Enum representation

Several authors at once suggest a similar approach:

Consider briefly their device on the example of json-swift:
  1. A new enumeration type (enum) ( github ) is introduced to represent JSON objects.
  2. Using the associated values mechanism, primitive Swift types are set to the possible values ​​of this enumeration, corresponding to JSON types ( github )
  3. The main type constructor checks the type of the passed object and returns an instance with the desired enumeration 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 transformation from JSON enumeration into the corresponding primitive Swift type, computed properties are used , which return the associated value only in the case of a type match ( github )

Transforming the original object model into such a view, 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 ensures that there are no runtime errors. When parsing an object with an inappropriate structure, the value of the entire expression will be nil (except when accessed by an index beyond the array boundaries).

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

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

Lazy handling

Another approach to solving the full conversion problem is to use “lazy” logic to check and typecast when traversing JSON. Instead of re-creating the entire JSON hierarchy at once with the values ​​of the necessary types, you can do it “in depth” at each step - only for one requested element. The implementation of just such an approach offers 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 Swift compiler.

Decision

I still wanted to find a solution that does not use new types, but expands 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, the problems here are caused only by transitions to the elements of dictionaries and arrays. This is because a subscript call ( [1] or [“number”] ) imposes a requirement on the type of value to which it applies - in our case, we refer to NSDictionary or to NSArray. Or, on the other hand, the resulting values ​​from NSArray and NSDictionary are of type AnyObject, which requires a type conversion for use later in the call chain.

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

 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 via a subscript (with an Int or String parameter). As a result, you can get either a collection element (if the object is a collection, its type corresponds to the subscript type 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 the JSONValue. Swift allows you to add protocol implementations via 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:

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

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

Although this is not the most compact option considered, this solution fully meets all the listed requirements.

Alternative option

Based on the same idea, a variant is possible using the protocol with the @objc attribute. This allows the use of explicit type conversions instead of an auxiliary function, but prohibits the use of subscripts — instead, they will have to use conventional methods. 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 subscripts version. Someone may be confused by the number of question marks, but on the other hand, each use of it is understandable and carries a meaning.

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

Code and usage examples are available on github .

Source: https://habr.com/ru/post/228949/


All Articles