A JSON Parsing Class Generator

At Houzz, we’ve developed a solution for JSON parsing in Swift that can save developers a lot of time while supporting all the great characteristics of Swift – namely, type safety, marking model properties as read only, and supporting pure Swift classes (not derived from Objective C) and Swift structs.

The difficulty with parsing JSON in Swift is that it is a strongly typed language, while JSON is loosely typed and JSON tags don’t carry type information and can carry a payload of any type. This requires quite cumbersome code to perform all the needed type checks and to handle all the fallbacks. An added difficulty comes from Swift’s strict adherence to its two-phase initialization, requiring you to initialize all class properties before calling methods on it.

There are several open source libraries already available for parsing JSON in Swift. The first type of libraries simply manage a lot of the complexity in type checking the JSON. These include libraries like Unbox, SwiftyJSON or JSONCodable. While they do reduce the complexity in parsing, they also involve writing a lot of boilerplate code, which we wanted to avoid. In addition, we wanted the solution to also support NSCoding and NSCopying out of the box, as our objective C-based solution does.

In objective C we used a runtime introspection-based solution, similar to JSONModel, except it is a homegrown solution tailored to our needs. The advantage of such a solution is that there is no boilerplate code needed. Just declare the properties you expect to get from the JSON and the runtime introspection will populate the properties based on the data read from JSON. Similar solutions exist in Swift, such as EVReflection. All you need to do is inherit from the EVReflection base object (which was another downside for us, as we wanted to control the class hierarchy) and define the properties you want to extract from the JSON. The downside is that in order for this to work, all the properties need to be defined as read/write, that is as var properties. We wanted to clearly mark in the code which properties came from our server and are part of our data model, and therefore shouldn’t be written to locally. Swift requires these to be var properties due to the init phases in Swift, and the compiler verifying all properties were initialized in the class init. This limitation eliminated any dynamic runtime solution for us.

A third type of solution is graphical tools that can parse the JSON and code generate Swift classes from the JSON. SwiftyJSONAccelerator and JSONExport are examples of such tools. The problem with these solutions is that our JSON API evolves over time, so our objects add properties as the API and data model evolves. We wanted a quick and easy way to update our existing models. Also, being developers, using a text-based solution, preferably similar to source code we know, that is easily source-controlled, is more in our nature.

These led us to the solution we finally chose. We used a code generator to create all the boilerplate code for us, based on a Swift-like class definition, and used Xcode’s built-in ability to define build steps to automatically add the code generation into our build flow. Using this solution, we don’t spend time writing parsers, we just declare our model properties in the most natural syntax for developers, a Swift-like file.

The Cast File We define our classes in a .cast file which has a Swift-like syntax. It will also use the Xcode syntax highlighting, and as we’ve pointed out, properties can be either defined as let or var. So let’s jump into the first, simplest class example:

class Person: NSObject, DictionaryConvertible {
    let firstName: String
    let lastName: String = "x"
    let middleName: String?
    let yearsOld: Int //! "age"
}

This defines a Person class. It derives from NSObject, and by indicating that it conforms to the DictionaryConvertible protocol, the code generator knows this is a class for which an init(dictionary:)? needs to be generated. The DictionaryConvertible protocol defines two methods, the init which gets a dictionary parameter (a dictionary that is the result of NSJSONSerialization), and a DictionaryRepresentation method which does the opposite – given an object, it returns its dictionary representation. It also defines an overloaded init(json:)? Initializer that can get a JSON parameter either as a Data or as a String object.

The Person class defines a required property, firstName. This is non-optional – if the dictionary doesn’t have a “firstName” key, the init will fail and return nil. The Person class also contains a lastName optional property. If the dictionary doesn’t have a “lastName” key, the lastName will get a default value of “x”. The last property, “middleName,” is also optional. If the dictionary is missing this key, middleName will be nil. The last property, “yearsOld,” is an Int but is read from the dictionary key “age”. The //! is the form to add special directives to the code generator. So //! “Key” is how you override the default property name to key name mapping. You can also use this to pick up nested keys. //! “Outer/inner” will look in the dictionary for the key “Outer” which should have a dictionary value, this dictionary should have the key, “inner.” The reason we chose a failable initializer rather than a throwing one is since we do still have objective C in our code, and our models sometimes need to be called from objective C as well, a fail-able initializer is compatible while a throwing one isn’t.

We also wanted our model classes to automatically support NSCoding and NSCopying. Our objective C JSON serialization solution did that, and we find this useful, particularly in save state when you have to serialize the app’s data. To support NSCoding and NSCopying, simply declare your class as conforming to these protocols. The code generator will detect this and generate the required methods to conform to these protocols. In order to do this, your class must derive from NSObject, which raises another feature of the code generator – there is no need to derive from NSObject just for conformance to the DictionaryConvertible protocol. A pure Swift class will work as well. In fact, you can also declare your object as a struct.

Integrating into Xcode To build the cast file, one needs to add a build rule to Xcode. This is done from the build rules tab in the project settings as follows:

Now tapping build will create a .Swift file from the .cast file and compile it into the project.

The simplest way to add a cast file to your project is to add a Swift file using Xcode’s file template and rename it to .cast. Alternatively, if you add a cast file directly, remember to add it to the source files list in the project settings. To enable syntax highlighting in the file inspector, change the cast file type to Swift source code as shown.

To complete the integration, add the cast framework to your project.

The Cast Framework and Mapper Class The generated file will import the Cast framework. This framework defines a single class, Mapper, that has two class functions, map and unmap. The code generator is using the map function when converting values from the dictionary and assigning them to the property values. The map function is an overloaded function that get an AnyObject parameter and returns the requested type. The implementation of the Mapper class in the Cast framework supports the following types: String, Int, UInt, Float, Double, CGFloat, NSURL, and arrays of these types. It also supports any type that conforms to DictionaryConvertible, so you can easily create properties that are of other DictionaryConvertible types. If you want to add support for a new variable type, such as NSDate, extend the Mapper class and add a map(_:) implementation that returns the type you want. To continue the NSDate support example, if our JSON contains dates as number of seconds from 1970, our Mapper extension would be:

extension Mapper {
    func map(object: AnyObject) -> NSDate? {
        switch object {
        case n as Double:
            return NSDate(timeIntervalSince1970: n)
        default:
            return nil
        }
    }
}

Note that we opted not to support dates by default, as dates can be sent in various formats. We opted to let developers add support for the date format they are expecting.

Other Factors Enum Support When using properties of type enum, if the enum is backed by a basic type, say a string or an Int, and the enum is defined in the .cast file, support is automatic. The code generator will pick up the enum definition and generate the correct code to parse the underlying basic type, and initiate an enum with a raw value. For any other enum type, the Mapper class needs to be extended to add support for this type. See previous section on how to extend the Mapper class.

AwakeWithDictionary If in the class definition in the .cast file you add an

//! awake

comment, the code generator will add a call to an awakeWithDictionary(dictionary: [String: AnyObject]) -> Bool function, passing it the dictionary used to initialize the object. If this function returns false, the init call will fail. This is your last chance to perform any special data verification on the object received from the JSON.

Extending the model If you need to add functionality to your model, that is add methods, the easy way to do this is by adding a separate extension Swift file, and adding your func code there. While it is possible to place everything in the cast file, we find it inconvenient as Xcode doesn’t have its full code completion functionality when editing a .cast file.

You can add in class extensions functions or computed properties, but not stored properties. If you need a stored property that should be ignored by the code generator and will not generate code to try, parse it from the JSON dictionary, add an ignore comment to the property definition:

     var myProp: Int! //! ignore

Such properties have to be var properties. They need to be either optional, implicitly unwrapped optional or non-optional with an initialized value, I.e.

     var myProp: Int = 0

Otherwise the Swift compiler will complain that the generated init method is not initializing all properties.

Inheritance If defining a class that inherits from a DictionaryConvertible class, the DictionaryConvertible deceleration can’t be added to the class or struct declaration. Any class in a .cast file that doesn’t contain the DictionaryConvertible protocol conformance deceleration is assumed to be inheriting from one that does, so the init method generated will call super to initialize the super class.

Since NSCoding conformance is optional, the code generator is not clever enough to figure out if the super class is conforming to it, nor does it have visibility into other .cast files. To tell the code generator the class needs to add NSCoding conformance, add a special comment to the class declaration:

//! NSCoding

The code generator will then generate the needed methods to add NSCoding conformance and call super.

Nothing needs to be done to add conformance to NSCopying since NSCopying is realized using the NSCoding methods.

Command Line Options The following command line options may be specified in the Xcode build rule when invoking the script to further control its behavior:

-c By default the key names that are searched in the dictionary are identical to the property names, so a property named firstName will be instantiated from the dictionary using the key “firstName.” If the -c option is provided, the key names will use an uppercase first letter by default, so “FirstName.”

-i Will make the key lookup in the dictionary case insensitive.

-n Will make empty strings in the JSON (i.e. ”“) treated like nil values.

About the Script The script is written in Swift. Being Swift developers, this was the most natural choice and the one we felt will best leverage our skills. Written in Swift, it could easily be compiled into a binary command line tool (in fact, this is what the unit tests are doing), but the advantage of a script is that zero setup needed before building your project, and there’s no need to compile a command line tool first.

Swift 3 Ready The script is Swift 3 ready if you have already moved over to Swift 3. There is a Swift 3 branch in the code that you can use if you want the code generator output to be Swift 3. Remember to switch your Xcode command line tools environment to Xcode 8 beta if you are trying the script on Xcode 8 using the xcode-select command.

Code The code for JSON cast is available on github here.

#building #buildinghouzz #guyshaviv

Recent Posts

See All

Scaling Data Science

Being the first Data Scientist (DS) at a startup is exciting, yet comes with a myriad of challenges from navigating data infrastructure and data engineering staffing to balancing proper modeling again

Copyright : Guru Interior & Decorators

ISO 9001:2015 Certified

MSME Registered Company

  • Instagram
  • LinkedIn
  • whatsapp-png-image-9
  • Facebook
  • Twitter
  • YouTube