The missing puzzle pieces for deserializing JSON in Swift 4

Since the release of Swift 4, the arguments for using third-party frameworks to work with JSON have been moot as the Swift community has settled on working with JSON via the Codable protocols in Foundation. Plenty of great tutorials have already been posted on how to take advantage of these additions, but unfortunately for me, there were missing pieces of the puzzle crucial for building a Swift SDK to interface with the Content Delivery API provided by Contentful. How could I use standard library to take a JSON array that had a heterogeneous mix of object types and spit out some types that I could actually use?

Working with an assorted mix of JSON items could be done in the old-fashioned way: turn the entire JSON tree into a dictionary then go from there. But as it turns out, using Codable to deserialize Dictionaries in Swift 4 presents its own set of challenges, and I had to write a whole other set of extensions to solve that problem. Even after all the work of deserializing arbitrary dictionary structures, I still needed to transform those dictionaries into model class instances and struggled with the complexities of resolving circular dependencies. All I wanted to do was take JSON and directly deserialize instances of model classes. The statically-typed world of Swift was taking this task, that would have been a breeze with a dynamic programming language, and giving me a headache!

Who cares about heterogeneous JSON collections anyway?

Not everybody will have the necessity to implement deserialization of heterogeneous collections from JSON like me. If you’re thinking: "why would I ever design my API to return an array of JSON objects of varying types?", consider the utility heterogeneity provides by looking at the API response to the following query to the Contentful Content Delivery API and its response body, by hitting the following path with cURL:

curl "https://cdn.contentful.com/spaces/cfexampleapi/entries?content_type=cat&access_token=b4c0n73n7fu1"

We get a collection of cat instances in the items array, and we also have an array of objects in includes, which are linked to from those cats. If we resolve the relationships contained in this JSON response, we can save ourselves API calls each time we want to access any of those related objects.

Deserializing a collection of mixed types

Most JSON APIs that return heterogeneous collections will ensure that the type information is present for each object, and the Contentful Delivery API is no different. Here is how the id of the content type for each JSON item is read so that we can call initializers for the appropriate model classes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var entriesJSONContainer = try self.nestedUnkeyedContainer(forKey: key)

var entries: [EntryDecodable] = []

while entriesJSONContainer.isAtEnd == false {

    let contentTypeInfo = try itemsAsDictionaries.contentTypeInfo(at: entriesJSONContainer.currentIndex)

    // For includes, if the type of this entry isn't defined by the user, we skip deserialization.

    if let type = contentTypes[contentTypeInfo.id] {

        let entryDecodable = try type.popEntryDecodable(from: &entriesJSONContainer)

        entries.append(entryDecodable)

    } else {

        // Another annoying workaround: there is no mechanism for incrementing the `currentIndex` of an

        // UnkeyedCodingContainer other than actually decoding an item

        _ = try? entriesJSONContainer.decode(EmptyDecodable.self)

    }

}

The above code assumes that the JSONDecoder instance has a [String: EntryDecodable.Type] available via its userInfo property.

We now have the type information stored in a variable, but the Swift compiler is going to make this a bit tricky for us and we'll yearn for some dynamic runtime magic of our long-lost relative, Objective-C. When assigning a type to a variable in Objective-C, you can turn around and use that type to make casts and call object constructors. In Swift, finding a way to take a type and dynamically look up its initializers is a bit more difficult — the trick is using a combination of protocol constraints and protocol extensions:

1
2
3
4
5
6
7
8
9
10
11
12
internal extension Decodable where Self: EntryDecodable {

    static func popEntryDecodable(from container: inout UnkeyedDecodingContainer) throws -> Self {

        let entryDecodable = try container.decode(self)

        return entryDecodable

    }

}

As I explain in the inline comments contained in the Contentful.swift SDK, the code above works around the fact that dynamic metatypes cannot be passed into initializers like UnkeyedDecodingContainer.decode(EntryDecodable.Type), but metatypes can invoke static method calls.

Returning to the earlier code snippet, we can see that we assigned the metatype to a variable type, then called the popEntryDecodable(from:) method to get an instance of that type:

1
2
3
4
5
6
if let type = contentTypes[contentTypeInfo.id] {

    let entryDecodable = try type.popEntryDecodable(from: &entriesJSONContainer)

}

What’s next

Now we’ve found a way to be flexible in handling responses for JSON APIs while ensuring type safety. We could have saved ourselves some client-side effort by avoiding heterogenous collections in our JSON API, but there are some niceties our approach affords: denormalized JSON structures have merits such as facilitating object de-duplication. Additionally, denormalized structures can be great for prototyping an API while it's still in infancy and is rapidly evolving.

Our framework is entirely protocol-based, opening up the doors to integrating with many other frameworks that do enforce inheritance such as Realm and CoreData. If you liked what read, check out the contentful.swift SDK — that’s ready-to-build on platforms like iOS, tvOS, macOS, watchOS and Linux.

Now that you’ve hopefully learned something new, check out the features page to see what Contentful’s content infrastructure can do for your web projects — and while you’re at that, sign up for a free Contentful account and give things a whirl.

Blog posts in your inbox

Subscribe to receive most important updates. We send emails once a month.