Using Codable With Nested JSON

Todd Kramer
Frame.io Engineering
3 min readJul 24, 2018

One of the core features of Frame.io is time-stamped comments and annotations. Users can draw directly on a frame of video or an image with different tools (pen, arrow, rectangle, line) and colors to clearly indicate what their comment refers to. Each “annotation” can have multiple “drawings”, each of which may use different tools or colors.

We return annotations as nested JSON. Here is an example (removing the geometry for simplicity):

"annotation": "[{\"tool\":\"pen\",\"size\":4,\"color\":\"#E74A3C\"},{\"tool\":\"arrow\",\"size\":4,\"color\":\"#E67422\"}]"

In this article we’ll explore two approaches to dealing with nested JSON when using Swift 4’s Codable protocols, with the goal of taking maximum advantage of Swift’s default implementations. The first approach is to use single value containers to move custom decoding logic into a separate type. The second is to store the nested JSON as a String and lazily decode it.

Single Value Containers

Here is the JSON response we need to serialize, representing a simplified version of a Frame.io comment response:

One way we could model this response is as follows:

Our CodingKeys can be automatically synthesized. Here we’re using an Annotation type to model the nested annotation JSON.

As mentioned above, an annotation is represented as an array of drawings. So first let’s look at the Drawing model:

In our codebase, Color is type-aliased as UIColor on iOS / tvOS / watchOS, NSColor on macOS. One way to have color conform to Decodable is to convert to and from hex values.

Drawing has an embedded enum, Tool, which has a string raw value. Both Drawing and Tool also conform to Decodable, enabling default implementations for Comment.

Now we’re ready to write the Annotation model and serialize the nested JSON using a single value container.

Here we are decoding the JSON string in three steps:

  1. Use the single value container to decode the nested JSON into a String.
  2. Convert the string value to data with UTF-8 encoding. If that fails, we throw an error.
  3. Use JSONDecoder to decode the data into a Drawing array.

Lazy Decoding

Since an annotation is just a collection of drawings, why have an Annotation type at all? The challenge is to still take advantage of Swift’s default Decodable implementation for Comment while also removing the extra type. We can change the Comment model as follows:

With this approach, we’re able to remove the Annotation type and use a JSONDecoder to serialize the nested JSON if necessary.

The Codable protocols introduced in Swift 4 made it easy in most situations to serialize models from JSON or property lists with little or no boilerplate code. Taking advantage of Swift’s default Decodable implementations where possible makes our code safer and easier to maintain. Using the approaches above, we can serialize models with nested JSON without adding significant custom decoding logic.

Ultimately, we decided to go with the lazy decoding approach. While it was nice on the one hand to have a container type (Annotation) that matched the response, recognizing that an annotation is in fact just a collection of drawings and removing the extra code felt like the cleaner implementation. Both approaches are valid, and which you choose will depend on what is most readable and maintainable for your application.

👋 like what you’ve read? We’re hiring!

At Frame.io, we’re powering the future of creative collaboration. Over 500,000 video professionals use Frame.io to seamlessly share media and gather timestamped feedback from team members and clients. Simply put, we help companies create better video, together.

Across the stack we’re big users of AWS Lambda, Elixir, Swift, Go, and React. We’re a small, polyglot team that thinks big and works collaboratively to solve the biggest challenges for our customers that include Vice, BuzzFeed, Turner and NASA.

--

--