Improving error messages from generated JSON decoders

Over a year ago we started work on our first Swift iOS app for a client (PostNL). We needed good JSON parsing but we couldn’t find a library that satisfied our needs, so instead we wrote a code generator: JsonGen.

Today, we’ve updated JsonGen to produce good error messages when it can’t decode a piece of JSON.

What is JsonGen

Let’s start off with a quick introduction to how JsonGen works:

  1. Write a Swift struct representing your JSON:
struct Blog {
 let title: String
 let subTitle: String?
}
  1. Install JsonGen
  2. Run JsonGen on the file with the struct: swift-json-gen Blog.swift

A new file Blog+JsonGen.swift has been created with a decodeJson extension method for your struct. No extra manual work is needed!

The original approach

In the previous version of JsonGen, the type signatures of the generated decoders looked like this:

static func decodeJson(json: AnyObject) -> Blog?

Because the JSON might not match the Blog type we are trying to decode, the decoder can fail. That’s why it returns an optional value. To help with debugging, the generated code contains assertionFailures, pointing to the problem with the JSON.

During development of our app, the JSON decoders would sometimes fail on an assertion. For one of two reasons: either our struct was wrong, or the server producing the JSON had a bug. In the first situation we would fix our error, in the second we would politely ask the backend team to fix their error. All was fine.

And then our app went live.

Production builds remove assertions; so all we are left with, when JSON fails to decode, is a nil. There’s nothing we can do for our users when a decode error happens in production, except put up an unhelpful error message: “Unable to handle server data”. We can’t even log the error to the error logging service, because we don’t know what the error is.

New feature: error information

The new version of JsonGen produces decoders with this type signature:

static func decodeJson(json: AnyObject) throws -> Blog

No more optional; instead we throw an error when something goes wrong. The error is quite descriptive of the problem. This is the complete error type:

public enum JsonDecodeError : ErrorType {
  case MissingField
  case WrongType(rawValue: AnyObject, expectedType: String)
  case WrongEnumRawValue(rawValue: AnyObject, enumType: String)
  case ArrayElementErrors([Int: JsonDecodeError])
  case DictionaryErrors([String: JsonDecodeError])
  case StructErrors(type: String, errors: [String: JsonDecodeError])
}

As you may have noticed, this error type can contain multiple sub-errors. We don’t stop decoding the JSON when we encounter a problem. We try to decode as much of the JSON as possible, so we can report all problems, not just the first.

Example 1

To start off with the happy flow, this parses some JSON without any errors:

do {
  let str = "{ "title": "Tom's Blog", "subTitle": "Sup?" }"
  let data = str.dataUsingEncoding(NSUTF8StringEncoding)!
  let json = try NSJSONSerialization.JSONObjectWithData(data, options: [])
  let blog = try Blog.decodeJson(json)

print(blog.title)
}
catch {
  print(error)
}

This prints: Tom's Blog

If the title field is removed from the JSON, it would instead print:

1 error in Blog struct
 - title: Field missing

Example 2

Now, for a more complicated example. The struct has been extended with some nested fields:

struct Blog {
  let title: String
  let subTitle: String?
  let posts: [Post]
}

struct Post {
  let title: String
  let body: String
  let author: Author?
}

struct Author {
  let name: String
  let email: String
}

And this is the JSON we feed the decoder. It has all sorts of problems:

{
  "title" : "My First Blog",
  "subTitle" : 42,
  "posts" : [
    {
      "title" : "Hello World",
      "author" : {
        "name" : "Tom",
        "email" : "tom@q42.nl"
      },
      "body" : "Hi"
    },
    {
      "body" : "Second"
    },
    {
      "title" : true,
      "author" : { },
      "body" : 42
    }
  ]
}

Result:

2 errors in Blog struct
 - subTitle: Value is not of expected type String?: `42`
 ▿ posts: 2 errors in array
    ▹ [1] (1 error in nested Post struct)
    ▹ [2] (3 errors in nested Post struct)

By default, the error is smartly truncated, so as not to blow up your logs when there’s no relation at all between the input JSON and the expected struct. However, you can still get the full error by using error.fullDescription:

2 errors in Blog struct
 - subTitle: Value is not of expected type String?: `42`
 ▿ posts: 2 errors in array
    ▿ [1] 1 error in Post struct
       - title: Field missing
    ▿ [2] 3 errors in Post struct
       - title: Value is not of expected type String: `1`
       - body: Value is not of expected type String: `42`
       ▿ author: 2 errors in Author struct
          - name: Field missing
          - email: Field missing

Conclusion

We just integrated this new version of JsonGen in several of our apps here at Q42, and we’re already quite happy with the errors we’ve seen. Insofar as you can be happy to see errors of course…

If you’re using JsonGen, upgrade to the latest version to try out the new error feature. Please let us know of any comments or issues over on Github.


Check out our Engineering Blog for more in depth stories on pragmatic code for happy users!