Given a key, decode a single value from a JSON using DecodingContainer

John Arnaoutakis
6 min readNov 13, 2022

In this article we are going to explore the flexibility that the decoding containers are giving us and we are going to create a method that decodes a single value from a JSON object, given the key without having to decode the whole object. First of all let’s explore what types of containers are available.

KeyedDecodingContainer: A concrete container that provides a view into a decoder’s storage, making the encoded properties of a decodable type accessible by keys.

UnkeyedDecodingContainer: A type that provides a view into a decoder’s storage and is used to hold the encoded properties of a decodable type sequentially, without keys.

SingleValueDecodingContainer: A container that can support the storage and direct decoding of a single nonkeyed value.

These are the containers and their declaration in Apple’s documentation. As you may already have guessed we are going to use the KeyedDecodingContainer because it provides us with the ability to extract the keys and filter out the one we are looking for. Of course you could do that by defining an enumeration and the key you want to decode but we are going to do something a bit more flexible here.

Let’s start by creating a struct that conforms to the CodingKey protocol. We are going to use this struct as the type that our containers are keyed by. This way we don’t have to pre-define our keys in enumeration and we can just decode using this struct.

struct DynamicCodingKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}

Now that we have a struct which we can use to dynamically generate a container that is going to provide us with keys, let’s consider the following JSON.

{
"name": "John",
"surname": "Appleseed"
}

Let’s assume that we are going to decode the surname key only and to do that let’s create a new function in an extension of the KeyedDecodingContainer. We are going to provide the value of the key as a string and we will use generics to make this work for all of the types that are decodables.

extension KeyedDecodingContainer {
mutating func decode<T: Decodable>(for key: String) -> T? {
// Find the first key that matches the given key.
// if it exists decode it as usual otherwise return nil
guard let firstKey = allKeys.first(where: { $0.stringValue == key })
else { return nil }
return try? decode(T.self, forKey: firstKey)
}
}

Awesome! Let’s try to decode only the surname now in our case using a struct.

struct Surname: Decodable {

var value: String?

init(from decoder: Decoder) throws {
var container = try? decoder.container(keyedBy: DynamicCodingKey.self)
value = container?.decode(for: "surname")
}
}

let jsonString = """
{
\"name\": \"John\",
\"surname\": \"Appleseed\"
}
"""
let data = jsonString.data(using: .utf8)!
print("surname \(try? JSONDecoder().decode(Surname.self, from: data))")

Job done! Well not quite, let’s consider the following JSON.

{
"name": "John",
"surname": "Appleseed",
"salary": {
"gross": 1200,
"net": 800
}
}

If you try to decode the gross key you may realise that our code doesn’t work as expected! Let’s update our decode function to handle this as well.

mutating func decode<T: Decodable>(for key: String) -> T? {
// Find the first key that matches the given key.
if let firstKey = allKeys.first(where: { $0.stringValue == key }) {
// if it exists decode it as usual otherwise return nil
return try? decode(T.self, forKey: firstKey)
} else {
// iterate through the coding keys available
for codingKey in allKeys {
// try to get the nested keyed decoding container
// and call the same function on the new contaner.
guard var nestedContainer = try? nestedContainer(
keyedBy: DynamicCodingKey.self, forKey: codingKey),
let decodedValue: T = nestedContainer.decode(for: key)
else { continue }
// return the decoded value
return decodedValue
}
}
return nil
}

Now that we updated our function, if we don’t find the given key in the first layer of the JSON we are going deeper for each key that we have and try to find our key in one of the nested containers. If we do find it then we decode it and return the value. Now if we try again to decode for the key gross, you are going to get the vaue 1200.

struct Gross: Decodable {

var value: Int?

init(from decoder: Decoder) throws {
var container = try? decoder.container(keyedBy: DynamicCodingKey.self)
value = container?.decode(for: "gross")
}
}

let jsonString = """
{
\"name\": \"John\",
\"surname\": \"Appleseed\",
\"salary\": {
\"gross\": 1200,
\"net\": 800
}
}
"""
let data = jsonString.data(using: .utf8)!
print("gross \(try? JSONDecoder().decode(Gross.self, from: data))")

Job done! Well again, not quite, let’s consider the following JSON.

{
"name": "John",
"surname": "Appleseed",
"salary": {
"gross": 1200,
"net": 800
},
"favourite_movies": [
{
"title": "Star Wars: Return of the Jedi",
"produced": 1983
},
{
"title": "Star Wars: The Empire Strikes Back",
"produced": 1980
},
]
}

If we try to fetch John’s favourite movie title by trying to decode for the key title then we are going to get a nil value. We need to update our function to handle unkeyed decoding containers because the favourite_movies key isn’t an object but an array. Let’s update our function one more time to handle this case as well.

mutating func decode<T: Decodable>(for key: String) -> T? {
// Find the first key that matches the given key.
if let firstKey = allKeys.first(where: { $0.stringValue == key }) {
// if it exists decode it as usual otherwise return nil
return try? decode(T.self, forKey: firstKey)
} else {
// iterate through the coding keys available
for codingKey in allKeys {
// try to get the nested keyed decoding container
// and call the same function on the new contaner.
if var nestedContainer = try? nestedContainer(
keyedBy: DynamicCodingKey.self, forKey: codingKey),
let decodedValue: T = nestedContainer.decode(for: key) {
return decodedValue
// try to get the nested unkeyed decoding container
} else if var nestedContainer = try? nestedUnkeyedContainer(
forKey: codingKey),
let decodedValue: T = nestedContainer.decode(for: key) {
return decodedValue
}
}
}
return nil
}

Great now we are also handling the case of the unkeyed decoding container. Although your code may not compile right now because nestedUnkeyedContainer returns an object of type UnkeyedDecodingContainer and we are missing our custom implementation for UnkeyedDecodingContainer. Let’s add that as well.

extension UnkeyedDecodingContainer {
mutating func decode<T: Decodable>(for key: String) -> T? {
// Try to get the nested keyed container of this unkeyed container
// call our function on the keyed container
guard var container = try? nestedContainer(
keyedBy: DynamicCodingKey.self) else { return nil }
return container.decode(for: key)
}
}

Great! You can use the following code to test that we are actually getting John’s favourite movie.

struct FavouriteMovieTitle: Decodable {

var value: String?

init(from decoder: Decoder) throws {
var container = try? decoder.container(keyedBy: DynamicCodingKey.self)
value = container?.decode(for: "title")
}
}

let jsonString = """
{
\"name\": \"John\",
\"surname\": \"Appleseed\",
\"salary\": {
\"gross\": 1200,
\"net\": 800
},
\"favourite_movies\": [
{
\"title\": \"Star Wars: Return of the Jedi\",
\"produced\": 1983
},
{
\"title\": \"Star Wars: The Empire Strikes Back\",
\"produced\": 1980
},
]
}
"""
let data = jsonString.data(using: .utf8)!
let title = try? JSONDecoder().decode(FavouriteMovieTitle.self, from: data)
print("Favourite Movie \(title)")

Job done! For real this time :).

I hope that after reading this article I helped you get a better understanding on the subject of decoding containers that apple provides us. What is described in this article may not be the case for a real world scenario but I really like what decoding containers can do!

If you enjoyed this article, don’t forget to clap!

--

--