This article is also available in Chinese.
Decoding JSON in Swift is a huge pain in the ass. You have to deal with optionality, casting, primitive types, constructed types (whose initializers can also be optional), stringly-typed keys, and a whole bevy of other issues.
Especially in a well-typed Swift world, it makes sense to use a well-typed wire format. For the next project that I start from scratch, I’ll probably use Google’s protocol buffers (great blog post about their benefits here). I hope to have a report on how well it works with Swift when I have a little bit more experience with it, but for now, this post is about the realities of parsing JSON, which is the most commonly used wire format by far.
There are a few states-of-the-art when it comes to JSON. First, a library like Argo, which uses functional operators to curry an initializer.
extension User: Decodable {
static func decode(j: JSON) -> Decoded<User> {
return curry(User.init)
<^> j <| "id"
<*> j <| "name"
<*> j <|? "email" // Use ? for parsing optional values
<*> j <| "role" // Custom types that also conform to Decodable just work
<*> j <| ["company", "name"] // Parse nested objects
}
}
Argo is a very good solution. It’s concise, flexible, and expressive. The currying and strange operators, however, are somewhat opaque. (The folks at Thoughtbot have written a great post explaining it here.)
Another common solution is to manually guard let
every non-optional. This is a little more manual, and results in two lines for each property: once as to create non-optional local variable in the guard statement, and a second line to actually set the property. Using the same properties from above, this might look like:
class User {
init?(dictionary: [String: AnyObject]?) {
guard
let dictionary = dictionary,
let id = dictionary["id"] as? String,
let name = dictionary["name"] as? String,
let roleDict = dictionary["role"] as? [String: AnyObject],
let role = Role(dictionary: roleDict)
let company = dictionary["company"] as? [String: AnyObject],
let companyName = company["name"] as? String,
else {
return nil
}
self.id = id
self.name = name
self.role = role
self.email = dictionary["email"] as? String
self.companyName = companyName
}
}
This code has the benefit of being pure Swift, but it is quite a mess and very hard to read. The chains of dependent variables is not obvious from looking at it. For example, roleDict
has to be defined before role
, since it’s used in role
’s definition, but since the code is so hairy, it’s hard to see that dependency clearly.
(I’m not even going to mention the pyramid-of-doom nested if let
situation for parsing JSON from Swift 1. It was bad, and I’m glad we have multi-line if let
s and the guard let
construct now.)
When Swift’s error handling was announced, I was convinced it was terrible. It seemed like it was worse than the Result
enum in every way.
- You can’t use it directly: it essentially adds required language syntax around a
Result
type (that does exist, under the hood!), and users of the language can’t even access it. - You can’t chain Swift’s error model the way you can with
Result
.Result
is a monad, allowing it to be chained withflatMap
in useful ways. - Swift’s error model can’t be used in an asynchronous way (without hacking it, like providing an inner function that does
throw
that you can call to get the result), whereasResult
can be.
Despite all of these seemingly obvious flaws with Swift’s error model, a blog post came out describing a use case where Swift’s error model is clearly more concise than the Objective C version and easier to read than the Result
version. What gives?
The trick here is that using Swift’s error model, with do
/catch
, is really good when you have lots of try
calls that happen in sequence. This is because setting up something to be error-handled in Swift requires a bit of boilerplate. You need to include throws
when declaring the function, or else set up the do
/catch
structure, and handle all your errors explicitly. For a single try
, this is a frustrating amount of work. For multiple try
statements, however, the up-front cost becomes worth it.
I was trying to find a way to get missing JSON keys to print out some kind of warning, when I realized that getting an error for accessing missing keys would solve the problem. Because the native Dictionary
type doesn’t throw errors when keys are missing, some object is going to have to wrap that dictionary. Here’s the code I want to be able to write:
struct MyModel {
let aString: String
let anInt: Int
init?(dictionary: [String: AnyObject]?) {
let parser = Parser(dictionary: dictionary)
do {
self.aString = try parser.fetch("a_string")
self.anInt = try parser.fetch("an_int")
} catch let error {
print(error)
return nil
}
}
}
Ideally, with type inference, I won’t even have to include any types here. Let’s take a crack at writing it. Let’s start with ParserError
:
struct ParserError: ErrorType {
let message: String
}
Next, let’s start Parser
. It can be a struct
or a class
. (It doesn’t get passed around, so its reference semantics don’t really matter.)
struct Parser {
let dictionary: [String: AnyObject]?
init(dictionary: [String: AnyObject]?) {
self.dictionary = dictionary
}
Our parser will have to take a dictionary and hold on to it.
Our fetch
function is the first complex bit. We’ll go through it line by line. Each method on a class can be type-parameterized, to take advantage of the type inference. Also, this function will throw errors, which will let us get the failure data back:
func fetch<T>(key: String) throws -> T {
The next step is to grab the object at the key, and make sure it’s not nil. If it is, we will throw.
let fetchedOptional = dictionary?[key]
guard let fetched = fetchedOptional else {
throw ParserError(message: "The key \"\(key)\" was not found.")
}
The final step is add type information to our value.
guard let typed = fetched as? T else {
throw ParserError(message: "The key \"\(key)\" was not the correct type. It had value \"\(fetched).\"")
}
Finally, return the typed, non-optional value.
return typed
}
(I’ll include a gist and a playground at the end of the post with all the code.)
This works! The type inference from the type parameterization handles everything for us, and the “ideal” code that we wrote above works perfectly:
self.aString = try parser.fetch("a_string")
There are a few things that I want to add. First, a way to parse out values that are actually optional. Because this one won’t need to throw, we can write a simpler method. It unfortunately can’t have the same name as the above method, because the compiler won’t know which one to use, so let’s call it fetchOptional
. This one is pretty simple.
func fetchOptional<T>(key: String) -> T? {
return dictionary?[key] as? T
}
(You could make it throw an error if the key exists but is not the expected type, but I’ve left that out for brevity’s sake.)
Another thing we sometimes want to do is additional transformation to the object after its pulled out of the dictionary. We might have an enum’s rawValue
that we want to build, or a nested dictionary that needs to turn into its own object. We can take a block in the fetch function that will let us process the object further, and throw error if the transformation block fails. Adding a second type parameter U
allows us to assert that the product of the dictionary fetch is the same thing that goes into the transformation function.
func fetch<T, U>(key: String, transformation: (T) -> (U?)) throws -> U {
let fetched: T = try fetch(key)
guard let transformed = transformation(fetched) else {
throw ParserError(message: "The value \"\(fetched)\" at key \"\(key)\" could not be transformed.")
}
return transformed
}
Lastly, we want a version of fetchedOptional
that also takes a block.
func fetchOptional<T, U>(key: String, transformation: (T) -> (U?)) -> U? {
return (dictionary?[key] as? T).flatMap(transformation)
}
Behold: the power of flatMap
! Note that the tranformation block has the same form as the block flatMap
accepts: T -> U?
.
We can now parse objects that have nested items or enums.
class OuterType {
let inner: InnerType
init?(dictionary: [String: AnyObject]?) {
let parser = Parser(dictionary: dictionary)
do {
self.inner = try parser.fetch("inner") { InnerType(dictionary: $0) }
} catch let error {
print(error)
return nil
}
}
}
Note again how Swift’s type inference handles everything for us magically, and doesn’t require us to write any as?
logic at all!
We can also handle arrays with a similar method. For arrays of primitive types, the fetch
method we already will work fine:
let stringArray: [String]
//...
do {
self.stringArray = try parser.fetch("string_array")
//...
For arrays of domain types that we want to construct, Swift’s type inference doesn’t seem to be able to infer the types this deep, so we’ll have to add one type annotation:
self.enums = try parser.fetch("enums") { (array: [String]) in array.flatMap( {SomeEnum(rawValue: $0) })}
Since this line is starting to get gnarly, let’s make a new method on Parser
specifically for handling arrays:
func fetchArray<T, U>(key: String, transformation: T -> U?) throws -> [U] {
let fetched: [T] = try fetch(key)
return fetched.flatMap(transformation)
}
This will abuse the poorly-named-but-extremely-useful flatMap that removes nils on SequenceType
, and reduce our incantation at the call site to:
self.enums = try parser.fetchArray("enums") { SomeEnum(rawValue: $0) }
The block at the end is what should be done to each element, instead of the whole array. (You could also modify fetchArray
to throw an error if any value couldn’t be constructed.)
I like this general pattern a lot. It’s simple, pretty easy to read, and doesn’t rely on complex dependencies (the only one is a 50-line Parser type). It uses Swifty constructs, and will give you very specific errors describing how your parsing failed, useful when trying to dredge through the morass of JSON that you’re getting back from your API server. Lastly, another benefit of parsing this way is that it works on structs as well as classes, making it easy to switch from reference types to value types or vice versa at will.
Here’s a gist with all the code, and here’s a Playground ready to tinker with.