By Casey Liss

It’s been said that the whole of iOS development is turning JSON into UITableViews. There’s probably more truth to that than I care to admit.

Often times, especially when working on an app as part of a company’s in-house team, an iOS developer can work with the server/API team to come to an agreement what JSON will be used. Sometimes, especially when working as an independent developer, the API is “foreign”, and thus the JSON it emits is outside of an iOS developer’s control.

Occasionally, APIs will make choices for their JSON structures that make perfect sense in more loosely-typed languages, but for strongly-typed languages like Swift, these choices can be more challenging. The prime example of this is JSON’s heterogeneous arrays.

In JSON, it is completely valid to have an array of objects that are not alike. An extremely simple example could be [1, "two", 3.0]. In more typical examples, these arrays won’t hold primitives but rather objects, and each object in these heterogeneous arrays will typically have vastly different key/value pairs. It’s easy to store heterogeneous arrays in Swift as an array of Dictionaries, but that… isn’t very Swifty.

What’s the more Swifty version, then? Preferably using Decodable?

This post is my attempt to describe exactly that.

Let’s assume you’re hitting some web API that describes a restaurant. It will return a restaurant’s name, as well as its menu, which is a heterogeneous array of objects that represent drinks, appetizers, and entrees.

Let’s further assume that the developers of this API made some annoying choices about how to name things, such that there’s no clear and easy way to make a base protocol that all the menu items can inherit from. 😑

So, some example JSON may look like this:

{
    "name": "Casey's Corner",
    "menu": [
        {
            "itemType": "drink",
            "drinkName": "Dry Vodka Martini"
        },
        {
            "itemType": "drink",
            "drinkName": "Jack-and-Diet"
        },
        {
            "itemType": "appetizer",
            "appName": "Nachos"
        },
        {
            "itemType": "entree",
            "entreeName": "Steak",
            "temperature": "Medium Rare"
        },
        {
            "itemType": "entree",
            "entreeName": "Caesar Salad"
        },
        {
            "itemType": "entree",
            "entreeName": "Grilled Salmon"
        }
    ]
}

The restaurant, Casey's Corner, serves two drinks, a Dry Vodka Martini and a Jack-and-Diet. It serves one appetizer, Nachos. It serves three entrees, Steak (which has an associated temperature), Caesar Salad, and Grilled Salmon.

The type of each menu item is defined as part of the menu item itself, using the itemType key. Note that we don’t particularly care about this key in our Swift objects, as their type will implicitly give us this information. We don’t want to clutter our plain old Swift objects with an itemType property.

How can we represent this in Swift? Most of this is straightforward:

struct Drink: Decodable {
    let drinkName: String
}

struct Appetizer: Decodable {
    let appName: String
}

struct Entree: Decodable {
    let entreeName: String
    let temperature: String?
}

struct Restaurant: Decodable {
    let name: String
    let menu: [Any]
}

Note that the menu property is Array<Any>. Again, in most examples, you’d probably be able to avoid this, and figure out some sort of common base type instead. I wanted to have a clear, bare-bones example that shows how to do this all by hand, no base types allowed.

For Drink, Appetizer, and Entree, we can rely on the automatically synthesized Decodable implementations: no further work required.

Restaurant is a whole different story, however.

The thing is, we need to be able to peek into the menu JSON array in order to see itemType for each menu item, but then we need to back up and actually decode each menu item. This gets wonky fast.

Here’s how I did it:

struct Restaurant: Decodable {
    let name: String
    let menu: [Any]
    
    // The normal, expected CodingKey definition for this type
    enum RestaurantKeys: CodingKey {
        case name
        case menu
    }
    
    // The key we use to decode each menu item's type
    enum MenuItemTypeKey: CodingKey {
        case itemType
    }
    
    // The enumeration that actually matches menu item types;
    // note this is **not** a CodingKey
    enum MenuItemType: String, Decodable {
        case drink
        case appetizer
        case entree
    }
    
    init(from decoder: Decoder) throws {
        // Get the decoder for the top-level object
        let container = try decoder.container(keyedBy: RestaurantKeys.self)
        
        // Decode the easy stuff: the restaurant's name
        self.name = try container.decode(String.self, forKey: .name)
        
        // Create a place to store our menu
        var inProgressMenu: [Any] = []
        // Get a copy of the array for the purposes of reading the type
        var arrayForType = try container.nestedUnkeyedContainer(forKey: .menu)
        // Make a copy of this for reading the actual menu items.
        var array = arrayForType
        
        // Start reading the menu array
        while !arrayForType.isAtEnd {
            // Get the object that represents this menu item
            let menuItem = try arrayForType.nestedContainer(keyedBy: MenuItemTypeKey.self)
            // Get the type from this menu item
            let type = try menuItem.decode(MenuItemType.self, forKey: .itemType)
            
            // Based on the type, create the appropriate menu item
            
            // Note we're switching to using `array` rather than `arrayForType`
            // because we need our place in the JSON to be back before we started
            // reading this menu item.
            switch type {
            case .drink:
                let drink = try array.decode(Drink.self)
                inProgressMenu.append(drink)
            case .appetizer:
                let appetizer = try array.decode(Appetizer.self)
                inProgressMenu.append(appetizer)
            case .entree:
                let entree = try array.decode(Entree.self)
                inProgressMenu.append(entree)
            }
        }
        
        // Set our menu
        self.menu = inProgressMenu
    }
}

The trick here is just before, and within, the while loop. Like we realized above, we need to be able to peek into each object in the menu array. However, in doing so, we advance the position of the arrayForType container, which means we can’t back up and grab the whole object for parsing into a Swift object.

Thus, we make a copy of arrayForType container, called array, which we use to decode full objects. When the arrayForType container advances, while we decode itemType, array does not advance. This way, when we try to decode() our Drink, Appetizer, or Entree, arrayForType has moved on, but array has not.

I’ve put all this in a gist on GitHub. I’m sure that other Swift developers will have thoughts on my approach here; if you’re that person, please feel free to either leave a comment on the gist, or fork it.