-
-
Save michaelbiggs/e1c09cbf6d78a0e9f7b32277fa8cda07 to your computer and use it in GitHub Desktop.
Decoding a heterogenous JSON array with unknown object types
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is intended to be dropped in a Playground. | |
import Foundation | |
let json = | |
""" | |
{ | |
"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" | |
}, | |
{ | |
"itemType": "dessert", | |
"entreeName": "Crème Brûlée" | |
} | |
] | |
} | |
""" | |
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] | |
// 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 | |
case unknown | |
init(from decoder: Decoder) throws { | |
self = try MenuItemType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown | |
} | |
} | |
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) | |
case .unknown: | |
let itemTypeString = try menuItem.decode(String.self, forKey: .itemType) | |
print("Unknown menu item type: \(itemTypeString)\n") | |
} | |
} | |
// Set our menu | |
self.menu = inProgressMenu | |
} | |
} | |
let data = json.data(using: .utf8)! | |
let restaurant = try! JSONDecoder().decode(Restaurant.self, from: data) | |
print("\(restaurant.name)") | |
for item in restaurant.menu { | |
if let d = item as? Drink { | |
print(" +-- Drink: \(d.drinkName)") | |
} else if let a = item as? Appetizer { | |
print(" +-- Appetizer: \(a.appName)") | |
} else if let e = item as? Entree { | |
print(" +-- Entree: \(e.entreeName)") | |
if let temp = e.temperature { | |
print(" Temperature: \(temp)") | |
} | |
} | |
} | |
/* Expected output: | |
* Casey's Corner | |
* +-- Drink: Dry Vodka Martini | |
* +-- Drink: Jack-and-Diet | |
* +-- Appetizer: Nachos | |
* +-- Entree: Steak | |
* Temperature: Medium Rare | |
* +-- Entree: Caesar Salad | |
* +-- Entree: Grilled Salmon | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment