Skip to content

Restoring your sanity reading JSON into Codables

Integrating your app with APIs means you’re going to be dealing with parsing JSON in data objects you can use nicely. iOS provides the Codable protocol to do just that thing, but once you start trying to map JSON to that well-formed protocol, that’s when things fall apart.

JSON doesn’t have standard schemas (at least none that were ever seriously adopted), so you’re stuck with reading documentation like this for the details:

😒 Great, that’s useful… One example response, no field descriptions, types, anything. (And before you ask, this is the GitHub API documentation, so not even large organizations are immune to this problem.)

So you code your app off of these examples, then when you try and use it in the wild, you get:

I followed their documentation, and it didn’t work. How the heck am I ever going to figure out how to make this API JSON into a nice, easy to use Swift struct?!

Thankfully, there are some tricks — and one tool — you can use to make your job a lot easier. We’ll start by deciphering those error messages above, and how to figure out what error they are trying to point out. Then we’ll cover a procedure I like to use whenever I encounter one of these errors to fix them, and even use to avoid these errors in the first place. Let’s dive in.


Deciphering the error messages

First, let’s talk about the error messages and figure out what they’re trying to tell us.

Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "body", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"body\", intValue: nil) (\"body\").", underlyingError: nil)): file JSONCodable.playground, line 101

There’s a lot to unpack in that error message above, and it isn’t the best formatted, but the important bit is in the first part after “raised an error”: Swift.DecodingError.keyNotFound. This tells you that the decoder was expected a value for a particular key, but it wasn’t present. The codingPath tells you where in the JSON the decoding failed — which in this case, the codingPath is empty, meaning it was looking in the root object — and the stringValue tells you it was looking for a key “body” and it was not present.

Just knowing this is the error gives you the solution: the “body” property in your Codable object is most likely not an Optional, but it should be. Make the one character change from a `String` to a `String?`, and you’ll find your problem is fixed.

So what kind of errors can we run into decoding a JSON? In this case, the Apple documentation helps us out. Note the Swift.DecodingError in the error message. This is an enum that contains all of the possible error types that a decoder can throw.

You can look at Apple’s documentation for more information, but I will summarize the short list here:

KeyNotFound:
I would say this is by far the most common error I see when trying to make Codables from JSON from third-party APIs, because it is usually so difficult to figure out whether a key is going to be required or not. This error is pretty much what it says in the description: your Codable was expecting a value for a particular key, but the key wasn’t found in the JSON object. You can change your key to an Optional type, and that should resolve this.

ValueNotFound:
Very similar to the keyNotFound error above, this is a case where the key was actually found in the JSON object, but it had a value of “null,” equivalent to nil in Swift. Because that requires the type to be optional, if the type in your Codable is not optional, you receive this error.

TypeMismatch:
This error occurs when your Codable has a particular type, but when the decoder was parsing the JSON, it found a value with a different type. This can mean one of two things: you just got the type wrong in your Codable, you fix it, and everything works great. However, because JSON doesn’t have any strict rules about what types values can take on, and no schemas to guide you, you can have a list of JSON objects where the value for “key1” is a string in one object, a list in the next object, not present in the next object, and a full JSON object in the next one. This is a rare situation, but it does exist, because some API writers really hate their users. You can fix this in your Codable, but let’s leave that solution until the next section, where we’ll have a tool to help out…

DataCorrupted:
This last error is more of a catch-all error for when the parsing fails because the Decoder can’t parse the JSON. This can either mean the JSON is simply invalid, but there are some specific cases where the JSON itself is valid, but because of how you have configured your JSONDecoder to handle certain value types, such as dates, if the format isn’t strictly matched, the decoder will throw this error.

Fixing the problems — or avoiding them in the first place

Usually once you run into the errors above, they’re generally easy to fix. But it’s not going to be a very good strategy to just keep throwing data at our Codable until it breaks again. Plus, JSON being the very loose, schemaless format it is, there’s always the possibility of running into an error above that it isn’t plainly obvious why it’s failing, or how to fix it. This is where having a standard technique for resolving these issues will help you not only fix the problems when you run into them, but avoid them in the first place by applying it from the beginning.

Collect data

To begin with, in order to find as many of the corner cases as possible, I try and collect as large of a response as I can of the target query. With a large response, I have a better chance of finding all of the corner cases within an API (optional fields, any strangely-typed fields, etc.).

Using the example GitHub project query above, I would search for a complementary query method in the API, because more than likely it is using the same structure. Then I would try a query that returns several hundred records or so. This will provide a representative sample of the JSON data that this API may return, that you would use in the next step later.

In the GitHub example from above, there is a query API available, described here, that will work nicely. They even provide a sample query that you can use to give you a JSON directly, no API key needed:

curl https://api.github.com/search/repositories?q=tetris+language:assembly&sort=stars&order=desc

As of the time of this post, it returns over 1600 results, which is a nice sample size to feed into the next step.

If you cannot find a query API that matches, the process becomes a little harder, because you will need to construct individual queries by hand for individual results, which means finding API IDs. Presumably there is a query that at least returns those, then you can write a script to download those results one by one. The good news is, all of those individual records will still work in the next step.

Use QuickType to analyze your result set

In the past, I’ve talked about the quicktype tool. As a quick summary, it takes one or more JSON objects as an input, and will produce code for an object model in a number of different languages, including Swift and Objective-C. This is what we will use to feed our data into. Let’s continue with the GitHub example.

I definitely recommend you follow along with the steps using the online version at https://app.quicktype.io, because the results are too big to include here.

First, access the URL example above for the GitHub projects list: https://api.github.com/search/repositories?q=tetris+language:assembly&sort=stars&order=desc. This will return a JSON; select all of it using Cmd+A, and copy with Cmd+C

Now, pull up https://app.quicktype.io. In the left pane select the JSON that’s there in the example to remove it, and Cmd+V paste in your GitHub JSON. The right pane code will change, and will now contain an object model that matches the sample you entered into the left pane.

When you investigate the model, you’ll even notice that it has attempted to apply some smarts to the object model: detecting fields that look like dates and making them Date fields, and even finding string fields that have limited sets of values and converting them into Swift enums. It’s really quite a great tool.

Use wholesale, or extract the bits you need

The easiest thing to do, of course, is take quicktype’s model and use it as is. It’s often the thing I do after reviewing it to make sure it looks good.

If you are correcting an existing object model, or the parsing you need to do doesn’t require a full object model, you may only want to pull a couple of pieces out of the code and integrate into your object model. The best thing to do is review the entire object model, look at the quicktype result for any surprises (like unexpected optional fields, strange types, etc.), and copy over the bits you need that don’t match up with what you originally wrote.

There is one final thing to mention about the model quicktype returns. As already mentioned multiple times, JSON is a freeform data format, and values can take on different types. Values in objects can be any type, values in list can be any type, etc. This means that sometimes quicktype returns code like this:

enum HasWiki: Codable {
    case bool(Bool)
    case integer(Int)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Bool.self) {
            self = .bool(x)
            return
        }
        if let x = try? container.decode(Int.self) {
            self = .integer(x)
            return
        }
        throw DecodingError.typeMismatch(HasWiki.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for HasWiki"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .bool(let x):
            try container.encode(x)
        case .integer(let x):
            try container.encode(x)
        }
    }
}

In this case, I forced this particular situation happen by changing one of the “hasWiki” boolean values in the sample data into an integer. This meant that hasWiki can either be a Bool or an Int. quicktype detects that from the sample, and generates this helper enum that acts as a container for both of those values, and gives you all of the code you need to encode/decode those values. It does more than just give you an object model; it manages all of those weird corner cases, giving you something easy, even for the weirdest of JSON. I suggest you give it a try the next time you run into JSON decoding errors.

Comments are closed.