If you’re developing a Swift application against an existing web API, you may feel a conflict between the structures of the API responses and the Swift model types that feel right for the app you’re developing. This is perfectly normal.

Your networking / decoding layer is there to take the filth from the outside world, and transform it into nice, usable model types for your app.

Decodable exists to help you transform data into specific types. The automatic functionality provided by the protocol is easy, and obvious. But there are plenty of common situations where what to do is not obvious. This article will show you how to deal with some of those situations while continuing to benefit from Decodable.

Scenario 1: The data I actually care about is nested inside the response

This is a commonly encountered response structure:

{
	"results": [ ... what I care about ... ],
	"some other stuff": { ... whatever ... }
}

some other stuff may be metadata about the request / response, for example. Very nice, but you don’t care about it.

You could deal with it by making a special container type, perhaps defined inline so it’s completely unambiguous:

// Here we are inside a function that is processing some response data
data -> [ThingsIWant] in

struct ResponseContainer: Decodable {
	let results: [ThingsIWant]
}

let container = try jsonDecoder.decode(ResponseContainer.self, from: data)
return container.results

That’s going to get a bit old if you have to do it for every single type. What you actually want is to be able to use something like this:

try jsonDecoder.decode([ThingsIWant].self, from: data, keyPath: ["results"])

How can this work? Well, Decoder can already give us a keyed container. If you make a CodingKey type that allows you to dynamically create keys, as I’ve written about before, and use that type in container(keyedBy:) you can treat the keyed container as a dictionary.

However, access to the primed decoder is only available inside an implementation of init(from decoder: Decoder), so you need a container type. This can use generics:

final class DecodableContainer<Contained: Decodable>: Decodable {

	let contained: Contained

	// The dynamic CodingKey type mentioned above
	struct Key: CodingKey {
		var stringValue: String
		init?(stringValue: String) {
			self.stringValue = stringValue
		}
	
		var intValue: Int? { return nil }
		init?(intValue: Int) { return nil }
	}

	init(from decoder: Decoder) throws {
		// TODO
	}
}

Inside the init method, you extract a keyed container, then a keyed container from that, for each entry in the key path. At the final key, decode the Contained type.

How do you make the key path available in the init method? You can’t change the signature, since that’s the thing that makes the type Decodable. This is where the decoder’s userInfo property becomes useful.

First, a small diversion. You could be forgiven for thinking that when you do this:

let decoder = JSONDecoder()
let thing = try decoder.decode(Thing.self, from: data)

Then this is true:

struct Thing: Decodable {
	init(from decoder: Decoder) throws {
		// decoder is my JSONDecoder, right? RIGHT??
	}
}

It is not. JSONDecoder is not a Decoder. What you get in the init method is a private type that implements Decoder. This is initially surprising, but makes sense when you think about it. The methods on Decoder only make sense when you’re actually in the process of decoding something, not when you’re kicking off the decoding process. However, the userInfo dictionary you set on JSONDecoder is available as the userInfo dictionary of the decoder you end up with in the init method, so that’s how you make the key path available.

You do this by two extensions, one on JSONDecoder and one on Decoder:

let keyPathKey = CodingUserInfoKey(rawValue: "keyPath")!

extension JSONDecoder {	
	private var keyPathToContainedType: [String] {
		set { userInfo[keyPathKey] = newValue }
		get { return userInfo[keyPathKey] as? [String] ?? [] }
	}
}

extension Decoder {
	var keyPathToContainedType: [String]? {
		return userInfo[keyPathKey] as? [String]
	}
}

With that small diversion over, you now have the ability to set the key path on the JSONDecoder and read it from the Decoder during the init method.

Here’s the implementation of the decode method with a key path that you wished for earlier, which is also added as an extension on JSONDecoder:

func decode<T: Decodable>(_ contained: T.Type, from data: Data, keyPath: [String]) throws -> T {
		keyPathToContainedType = keyPath
		let container = try decode(DecodableContainer<T>.self, from: data)
		return container.contained
	}

The method sets the key path, then calls the standard decode method on DecodableContainer. If that all went well, it returns the contained type, otherwise it will have thrown an error.

The final piece of the jigsaw is to implement the decode method for DecodableContainer:

init(from decoder: Decoder) throws {
	guard 
		let containerPath = decoder.keyPathToContainedType,
		containerPath.isEmpty == false  else {
		contained = try Contained.init(from: decoder)
	}
			
	var keys: [Key] = containerPath.map { 
		Key(stringValue: $0)! 
	}
	let finalKey = keys.removeLast()
	var nextContainer = try decoder.container(keyedBy: Key.self)
	while keys.isEmpty == false {
		let next = keys.removeFirst()
		nextContainer = try nextContainer.nestedContainer(keyedBy: Key.self, forKey: next)
	}
	contained = try nextContainer.decode(Contained.self, forKey: finalKey)
}

Here you progress through the key path, creating containers as you go, being left with the final container, which holds the data you’re interested in.

Scenario 2: I want some decoding to be able to fail without bringing down the whole process, and without ending up with lots of optionals

For example, you are getting an array of data from the API. Some of this data isn’t valid for your purposes. Perhaps there are null values in key fields. You could make your model type sufficiently flexible to be able to deal with invalid data, but that means you need lots of code down the line to filter out items you don’t want or handle optionals that shouldn’t be optional. Remember that the decoding layer is there to make API responses into the exact types you need.

If invalid data can’t be ingested because it would fail at decoding, then normally this:

stuff = try decoder.decode([Stuff].self, from: data)

Would throw if even a single element could not be decoded.

To contain errors thrown during decoding within the type, you can use a new type, which is analagous to Optional or Result:

enum FailableDecodable<T: Decodable>: Decodable {
	case decoded(T)
	case failed(Error)
  
	init(from decoder: Decoder) throws {
		do {
			let decoded = try T(from: decoder)
			self = .decoded(decoded)
		} catch {
			self = .failed(error)
		}
	}
  
	var decoded: T? {
		switch self {
		case .decoded(let decoded): return decoded
		case .failed: return nil
		}
	}
}

You can use this type during decode calls from decoders or containers:

allStuff = try.decoder.decode([FailableDecodable<Stuff>], from: data)

Then filter out the failures:

stuff = allStuff.compactMap { $0.decoded }

Scenario 3: My type is decodable, but the JSON keys for some fields are different depending on which API endpoint I’m calling

It’s possible your API developers are evil.

You can use the userInfo dictionary, as shown above, to pass context in to your decoding. Because you need to repeat userInfo conveniences for JSONDecoder and Decoder, you may prefer to create a new “Context” object with specified types, which is itself stored in the user info:

struct DecodingContext {
	let usingAlternateKeys: Bool = false
}

private let contextKey = CodingUserInfoKey(rawValue: "context")!

extension JSONDecoder {
	var context: DecodingContext? {
		get { return userInfo[contextKey] as? DecodingContext }
		set { userInfo[contextKey] = newValue }
 	}
}

extension Decoder {
	var context: DecodingContext? {
		return userInfo[contextKey] as? DecodingContext 
	}
}

let context = DecodingContext(usingAlternateKeys: true)
let decoder = JSONDecoder()
decoder.context = context
// Try some decoding...

With this technique you can add extra fields to the context without needing to change the user info extensions.

To use the context, you will need to manually implement init(from decoder:). Depending on the degree of difference, you can either include all possible keys in the CodingKey enum, or have two separate enums and create and decode from a container with the correct type. Here’s an example of the first option:

private enum Keys: String, CodingKey {
	case name
	case birthDate
	case alternateBirthDate = "birthday"
}

init(from decoder: Decoder) throws {
	let container = try decoder.container(keyedBy: Keys.self)
	name = try container.decode(String.self, forKey: .name)
	let dateKey: Keys
	if decoder.context?.usingAlternateKeys {
		dateKey = .alternateBirthDate
	} else {
		dateKey = .birthDate
	}
	birthDate = try container.decode(Date.self, forKey: dateKey)
}

Scenario 4: I need to know about decoded data item X, when I’m decoding data item Y

Say your response contains a couple of lists, and in your model it makes sense that items from one list are owned by items from another, or that you need items from the first list to ensure that you can set up the later items properly.

Decoding order is not the determined by the order in which items appear in the JSON file. It’s determined by the order you declare the properties in your types or, if you’ve added one, the order of the cases in CodingKeys This means you are in control of what is decoded when.

If you need certain items to be decoded up front, but don’t want to implement a manual init(from decoder:), then declare those properties first or put those keys at the top of CodingKeys. It would be worth backing this up with comments or unit tests, since it’s non-obvious and future you might re-group the properties during a refactor.

To get a reference to those items when decoding, you can use the context trick from the previous scenario, and store items in the context as you go along. For example, given this JSON:

{
	"sandwiches": [
		{ "bread": "White", "cheese": "American" },
		{ "bread": "Baguette", "cheese": "Vacherin" },
		{ "bread": "Farmhouse", "cheese": "Wensleydale" },
	],
	"cheeses": [
		{ "name": "Wensleydale", "strength": 1 },
		{ "name": "American", "strength": 0 },
		{ "name": "Vacherin", "strength": 4 }
	]
}

It would make sense to create an array of Cheeses and then assign actual types to each Sandwich property, instead of using the name. You’re looking for these types:

struct Menu: Decodable {
	let cheeses: [Cheese]
	let sandwiches: [Sandwich]
}

struct Cheese: Decodable {
	let name: String
	let strength: Int
}

struct Sandwich: Decodable {
	let bread: String
	let cheese: Cheese
}

Note that the Menu type declares cheeses before sandwiches. This means the decoder will attempt to decode the cheeses first.

Create a decoder context which can hold the cheeses as you’re going:

class DecoderContext {
	var cheeses = [Cheese]()
}
let decoder = JSONDecoder()
decoder.context = DecoderContext()

Now, to get the cheeses into the context, you’ll need to override an init(from decoder:) somewhere and manually add them. You could do it in Menu or Cheese, but here you’ll do it in Cheese. Implement the manual decoding as normal, then at the end:

decoder.context?.cheeses.append(self)

When it comes to decoding Sandwich, the decoder should now have all the appropriate cheeses. In fact, if a sandwich contains a cheese that doesn’t exist, or the developer has forgotten to attach a context that should throw an error. Here’s the code that Sandwich needs:

enum DecodingError: Error {
	case noCheese
}

private enum CodingKeys: String, CodingKey {
	case bread, cheeseName = "cheese"
}

init(from decoder: Decoder) throws {
	let container = try decoder.container(keyedBy: CodingKeys.self)
	bread = try container.decode(String.self, forKey: .bread)
	let cheeseName = try container.decode(String.self, forKey: .cheeseName)
	guard let cheese = decoder.context?.cheeses.first(where: { 
		$0.name == cheeseName 
	}) else {
		throw DecodingError.noCheese
	}
	self.cheese = cheese
}

Decodable follows the general principle of most Apple-provided APIs. Easy things are easy, and powerful things are possible. Don’t let the structure of a JSON response dictate how you design your app.

Richard Turton

Cocoa Engineer

MartianCraft is a US-based mobile software development agency. For nearly two decades, we have been building world-class and award-winning mobile apps for all types of businesses. We would love to create a custom software solution that meets your specific needs. Let's get in touch.