Working with complicated JSON response in Swift

Zahra Carthage
4 min readDec 14, 2022

--

Most often, we will want our swift Models to resemble the JSON produced in our response/external source as closely as possible.
However, there are times when the JSON we receive is nested several levels down and comes with dynamic/unkeyed values that we may consider not needed for our application, or maybe you’re only interested in only certain data from the JSON response that’s nonetheless hidden deep below.

In this post, we’ll go over how we can use nested containers to decode our nested JSON response into a flat struct using a custom init(from:) implementation.

Decoding nested JSON data into a one flat struct

Let’s consider the following JSON data:

{
"status":1,
"values":{
"9586504":{
"id":"9586504",
"firstname":"John",
"lastname":"Doe",
"placeOfBirth":"CA, USA",
"domain":[
"codebeautify.org",
"Medium.com/JohnDoe",
"Twitter.com/JohnDoe"
]
},
"689448": {
"id":"689448",
"firstname":"Jane",
"lastname":"Doe",
"placeOfBirth":"PH, USA",
"domain":[
"Facebook.com/JaneDoe",
"Twitter.com/JohnDoe"
]
}
}
}

Now in our case, we’re interested in the nested data (id, first name..) however there’s a lot of nesting here and it’s kind of messy.
We’re unable to change the response, so let’s see we can decode this Response into the following struct.

There’s a lot of nesting here, and in this case, all of this nesting is kind of noisy. We can’t change the backend, so let’s see how this JSON can be decoded into the following struct:

struct UserModel : Decodable {
let id: Int
let firstname: String
let lastname : String
let placeOfBirth: String
let domain: [String]
}

This user model does not represent our JSON at all, but a custom init(from:) can work wonders:

struct userModel: Decodable{

var id: String = ""
var firstname: String = ""
var lastname : String = ""
var placeOfBirth: String = ""
var domain: [String] = []

private enum userModelKeys: String, CodingKey{
case id
case firstname
case lastname
case placeOfBirth
case domain

}
init(from decoder: Decoder) throws {

if let userContainer = try? decoder.container(keyedBy: userModelKeys.self )
{
self.id = try userContainer.decode(String.self, forKey: .id)
self.firstname = try userContainer.decode(String.self, forKey: .firstname)
self.lastname = try userContainer.decode(String.self, forKey: .lastname)
self.placeOfBirth = try userContainer.decode(String.self, forKey: .placeOfBirth)
self.domain = try userContainer.decode([String].self, forKey: .domain)
}
}
}

In the init(from:) method, this first line should be familiar to you:

let userContainer = try? decoder.container(keyedBy: userModelKeys.self )

This line extracts a container that uses the keys in my UserModelKeys enum and then we are affecting each key to its corresponding data whilst precising the type.

self.id = try userContainer.decode(String.self, forKey: .id)
self.firstname = try userContainer.decode(String.self, forKey: .firstname)
self.lastname = try userContainer.decode(String.self, forKey: .lastname)
self.placeOfBirth = try userContainer.decode(String.self, forKey: .placeOfBirth)
self.domain = try userContainer.decode([String].self, forKey: .domain)

This means that I extracted the id, firstname.. and all my required data from the userContainer.

What we did until now is simple decoding of JSON response into a struct, if you’d try to decode your JSON response into the UserModel you’d most definitely get an error, that’s because we’re trying to access the data directly without decoding the first and second level that is a dynamic key and represents the ID of our user.

{
"status":1,
"values":{
"9586504":{
"id":"9586504",
"firstname":"John",
"lastname":"Doe",
"placeOfBirth":"CA, USA",
"domain":[
"codebeautify.org",
"Medium.com/JohnDoe",
"Twitter.com/JohnDoe"
],
//
},

In order to be able to do that, we’ll have to create another struct DecodedUsers that will hold our array of users that itself has to conform to decodable

struct DecodedUsers: Decodable {

var array: [userModel]

}

First, we’ll have to provide the enums which correspond to our nested elements, in our case it’s values and ID which is Dynamic.

{
"status":1,
"values":{
"9586504":{

//
},

So first we’ll create an enum for the top-level coding key: Values and another one for the second nested Coding key.

  private enum ResponseKeys: CodingKey {
case values
}
private struct DynamicCodingKeys: CodingKey {
//if our dynamic coding key is a String
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}

//if our dynamic coding key is an int and in our case
// and in our case, it is a string so we'll return nil.
var intValue: Int?
init?(intValue: Int) {
return nil
}
}

Now lastly, we’ll implement our init(from:) just like this.

  init(from decoder: Decoder) throws {

let outerContainer = try decoder.container(keyedBy: ResponseKeys.self)
let DynamicContainer = try outerContainer.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .values)

var tempArray = [userModel]()

for key in DynamicContainer.allKeys {
let decodedObject = try DynamicContainer.decode(userModel.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
array = tempArray
}

In this part, we’re first initializing a outerContainer container for the Values and another DynamicContainer that is a nested Container of our parent Container outerContainer .
Second, we’re creating a tempArray that holds a list of our UserModel.

The third step, We’re going to run through our dynamic nested Container DynamicContainer and decode the content into our UserModel and append our response to the TempArray we created earlier.
Lastly, we’re affecting the data in the tempArray to the Array that holds our UserModel.

Here is the full code.

struct DecodedUsers: Decodable {

var array: [userModel]

private enum ResponseKeys: CodingKey {
case values
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?
init?(intValue: Int) {
return nil
}
}

init(from decoder: Decoder) throws {

let outerContainer = try decoder.container(keyedBy: ResponseKeys.self)
let DynamicContainer = try outerContainer.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .values)
var tempArray = [userModel]()
for key in DynamicContainer {
let decodedObject = try DynamicContainer .decode(userModel.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
array = tempArray
}

}

And just like this both our models are ready and we’re ready to decode now on Json Response into DecodedUsers.

Here’s how it goes.

 let task = URLSession.shared.dataTask(with: request) {
data, response , error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "no data")
return
}
do {
let decodedResult = try JSONDecoder().decode(DecodedUsers.self, from: data)

}
catch let error {
print("Error decoding: ", error)
}
}
task.resume()

Conclusion

Working with nested JSON Responses can be a pain, especially if one of our nested containers is unkeyed and dynamic.

So during this tutorial, we adressed how to work with Both nested and dynamic keys.

That’s it for this one — thanks for reading.

--

--