// 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" | |
} | |
] | |
} | |
""" | |
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 | |
} | |
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 | |
} | |
} | |
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 | |
*/ |
@cliss Love this, thanks for sharing! A change I'd recommend for increased type-safety here would be to narrow the menu items based on a protocol. eg:
[...]
This way you can be confident that only types conforming toMenuItem
might show up in your menu, and an exception will be thrown if an unrecognized type is found.
We 100% agree, but I wanted to show an example of completely disparate types, which are possible in JSON.
You could also use an enum I guess to preserve the actual types.
Something like
enum MyEnum {
case drink(Drink)
case appetizer(Appetizer)
....
}
and then use that like
let menu: [MyEnum]
What do you think about that?
@dehlen Coincidentally, in the app I'm working on, that's precisely what I did. But that's not the point of the blog post nor this code snippet. :)
@cliss Great idea. Never thought to do this.
Another issue I run into often with foreign APIs is handling unexpected values in enums like MenuItemType
. If an itemType
value is returned that is something other than "drink", "appetizer", or "entree" then the entire Restaurant object fails to decode. That's typically not what I want to happen. So, I write a custom init to handle unknown
values.
https://gist.github.com/michaelbiggs/e1c09cbf6d78a0e9f7b32277fa8cda07
@michaelbiggs For sure.
Another issue I run into often with foreign APIs is handling unexpected values in enums like
MenuItemType
. If anitemType
value is returned that is something other than "drink", "appetizer", or "entree" then the entire Restaurant object fails to decode. That's typically not what I want to happen. So, I write a custom init to handleunknown
values.
If you run into this or similar issues caused by unstable or partially broken APIs fairly often, I can recommend the BetterCodable or EvenBetterCodable packages. With those you can say how to handle unknown or incorrect values, e.g. by returning nil for the specific value or by dropping it from the array.
@cliss Love this, thanks for sharing! A change I'd recommend for increased type-safety here would be to narrow the menu items based on a protocol. eg:
This way you can be confident that only types conforming to
MenuItem
might show up in your menu, and an exception will be thrown if an unrecognized type is found.