There’s plenty of articles out there about how to parse JSON in Swift, what libraries to use. Apple even posted one themselves as I was finishing up this post. What I’d like to talk about here is the way the uShip iOS app handles JSON parsing, which is a variation of Apple’s approach.
Sharing maintainable Swift code is a Challenge
Since its introduction just over two years ago, Apple’s Swift programming language has shown itself to be a language that can be really fun to work with. uShip’s iOS team chose to adopt the language very early on and in the process has had to learn to deal with some interesting challenges as a result.
One particular challenge comes with adopting 3rd party libraries. If you’re working with Swift, one thing to consider in adopting a library is how frequently such libraries are kept up to date. It’s important to consider how bad it may be if these libraries stop compiling when you have to move on to a new version of XCode. And unlike some other development environments, it is extremely challenging to produce and distribute reusable pre-compiled libraries. Instead it is common for shared code modules to be distributed as source code for others to compile themselves. Even if you use cocoapods, you’re still having to compile the pods yourself. Another dependency management system, Carthage, claims to support shared binary libraries, but until very recently this was only true if you were sharing the binaries with yourself on a single machine. This all becomes a pretty big issue when the actual source code language is changing drastically over time.
With all this in mind, sometimes it can be a big timesaver to replace a third-party library we’re using with simpler, in-house code that we can easily maintain ourselves. One place we’ve chosen to do this is in parsing JSON.
The iOS SDK doesn’t parse JSON for you
One thing that may surprise developers from other languages is that there’s no built-in way to instantly parse JSON objects into Data Transfer Objects (strongly typed data structures). There ARE some third party libraries out there for doing this: SwiftyJSON, and dankogai’s swift-json, among others. But as mentioned above, if you’re trying to avoid depending on 3rd party code, it’s worth considering doing it yourself.
As it turns out JSON parsing is not such a bad candidate for a more manual approach. Throughout the rest of this article, I’ll be sharing with you the technique we use in the uShip app for taking raw JSON data and converting it into strongly typed, Swift structs. These structs clearly expose the structure and meaning of data from a given endpoint. They are also easily composable and reusable with other similar endpoints, and can even help you build in sensible default values for missing values in a structure.
How we set up our Swift JSON parsing
Let’s step through the approach the uShip app is currently using.
Create A DTO Protocol
The first major piece of the puzzle was in creating a special protocol for all of our DTOs (Data Transfer Objects) to adopt, which simply specifies that these DTOs must have a constructor that allows them to be built with one of the basic JSON data types (Dictionary, Array, String, Number or Bool). At uShip all of our APIs provide NSDictionary or NSArray objects, so we’ll focus on those.
public enum JSONType {
case array(array:[AnyObject])
case dictionary(dictionary:[String:AnyObject])
//…
public static func create(jsonObject: AnyObject?) -> JSONType {
//…
}
}
public protocol JSONObjectConvertable {
init?(JSON: JSONType?)
}
Create Collection Extensions
The second part was in creating a set of extensions on the Swift Dictionary and Array types. These extensions have a set of functions which accept a key value or index into the collection object and return the strongly-typed value associated with that key or index. Through the power of Swift Generics, we’re able to give all of these functions the same exact name. Because of this, all you need to know in order to use these functions is that one function name. We also created one additional override of this function which could grab the collection value and return it as a type conforming to our JSON data protocol (described in the last paragraph). For example:
public extension Dictionary where Key: ExpressibleByStringLiteral, Value: AnyObject
{
//looks for a value with the given key
//if the value exists with the expected type, returns the value as that type
//returns nil otherwise
public func jsonValue<T>(_ key: String) -> T?
{
return (self[key as! Key] as? T)
}
//…
}
If you’re unused to Swift generics that code may be a little difficult to wrap your head around. The function jsonValue<T>(key:) works based on what you assign its return value to. If you assign the result to a string, it will return the dictionary value if it happens to actually be a string. If it isn’t a string, it will return nil. If you instead assign it to an NSNumber, it will only return the value if it is actually an NSNumber. If we want to pull out values of a non-object, primitive type like Float or UInt, we need more specialized overrides of this function.
public func jsonValue(_ key: String) -> Float?
{
return (self[key as! Key] as? NSNumber)?.floatValue
}
public func jsonValue(_ key: String) -> UInt?
{
return (self[key as! Key] as? NSNumber)?.uintValue
}
Create DTO Types
Finally we combine the first two pieces within a concrete DTO designed to mirror the expected contents of a JSON object. We do this by creating a struct, having it conform to our DTO Protocol, and then in the protocol-required constructor, we use our collection extensions to parse whatever data is passed in by the arguments.
struct User : JSONObjectConvertable {
var id : UInt?
var username : String?
init?(JSON: JSONObject?) {
guard let json = json else { return nil }
guard case .dictionary(let dictionary) = JSON else { return nil }
id = dictionary.jsonValue(“id”)
username = dictionary.jsonValue(“username”)
}
}
This shows everything really coming together. Our User DTO adopts the custom JSONObjectConvertable protocol, so it must have the two required initializers. In the second initializer, we ensure that the JSON object we build from is of the expected dictionary type. And finally we populate the “id” and “username” properties with our jsonValue extension function. Each call to jsonValue calls a different version of the function because we have one version that handles String optionals and one that handles UInt optionals.
Use the DTOs
Once this DTO is set up, we can put it to use wherever we get data back from a JSON document. Below we get data from an API endpoint and parse it using one of our DTO types:
let JSONObject = try? JSONSerialization.jsonObject(with: data, options:[])
let JSON = JSONType.create(jsonObject: JSONObject)
That’s it. And after you set up one endpoint, adding DTOs for more endpoints becomes increasingly easier. Most of the work is done by the collection extensions, which are reusable. Our actual networking code is a bit more complex than that, but explaining that is outside the scope of this particular article.
For more details on the code, check out our sample project, which uses this technique to parse JSON Data from NASA’s API for downloading photos from the Curiosity Mars Rover.
And if you want to try running the project for yourself, you might even see some cool martian landscapes!
Or Mars rover selfies.