Codable with nested JSON and multiple structs

You may read bunch of tutorials or blog sites explaining how to decode a JSON to the struct object in Swift. But most of them not explaining on how to use on multiple structs.

I am writing this as I have just discovered and don't want to forget it anymore. It took hours to look and peek the github repositories.

JSON string

Given this kind of json string:

{
	"error": 0,
    "message": "No error found",
    "user": {
    	"id": 1,
        "name": "Didats Triadi",
        "email": "didats@somewhere.com"
    	"address_detail": {
        	"address_area": "Sawojajar, Kedungkandang"
        	"address_city": "Malang",
            "address_country": "Indonesia"
        },
        "spouse_detail": {
        	"spouse_name": "Isyana Sarasyati",
            "spouse_email": "isyana@somewhere.com"
        }
    }
}

From the json above you are planning to have 3 structs. User, Address, and Spouse.

struct User {
	var id: Int,
    var name:  String,
    var email: String,
    var address: Address,
    var spouse: Spouse
}

While the Address and Spouse are:

struct Address {
	var area: String
    var city: String
    var country: String
}
struct Spouse {
	var name: String
    var email: String
}

What you gonna need is to add Codable protocol to each one of those structs above and add the CodingKey protocol on the enum. Starting with the Spouse and Address first as the easiest.

struct Spouse: Codable {
    var name: String
    var email: String
    
    private enum  CodingKeys: String, CodingKey {
        case name = "spouse_name"
        case email = "spouse_email"
    }
}

Almost the same as Spouse, the Address struct has something like this below:

struct Address: Codable {
    var area: String
    var city: String
    var country: String
    
    private enum  CodingKeys: String, CodingKey {
        case area = "address_area"
        case city = "address_city"
        case country = "address_country"
    }
}

You may have no problem with those 2 structs above, as they are the same as all tutorials you have been read. If you still can't understand, just google codable swift, you will get many articles explaining the struct.

As the main thing on this article, we will create the basic and move along with line by line on how to implement multiple struct with the nested JSON. As basic, we will get the basic setup on  Codable struct.

struct User: Codable {
	var id: Int,
    var name:  String,
    var email: String,
    var address: Address,
    var spouse: Spouse
    
    private enum CodingKeys: String, CodingKey {
    	case, id, name, email
        
        case user = "user"
        case address = "address_detail"
        case spouse = "spouse_detail"
    }
}

The struct above we specify the address will be using key address_detail and spouse using key spouse_detail.

Adding decoder

The next step is to add decoder init and explain how we got each key on the JSON.

// ... code on the header part
	init(from decoder: Decoder) trows{
    	
    }
}

Inside the initialize method, create a container variable which will take from the CodingKeys method we have created, then create user variable since the user data inside key  user on JSON.

let container = try decoder.container(keyedBy: CodingKeys.self)
let user = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)

And specify each variable with the data type and the codingkey option.

id = try user.decode(Int.self, forKey: .id)
name = try user.decode(String.self, forKey: .name)
email = try user.decode(String.self, forKey: .email)

And we will have different approach for Address and Spouse

let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
address = try Address(from: data.superDecoder(forKey: .address))
spouse = try Spouse(from: data.superDecoder(forKey: .spouse))

Notice how we send a decoder to the Spouse and Address struct.

Important!

The most common mistakes was on the creating a struct object for other struct. You need to noticed that the variable data above took from the container, not the user. Because the address was under container -> user -> address.

Variable data will try to get into the user key, then each address and spouse will get the key under the user key.

So the complete code to test on your Playground is:

import Foundation

struct Address: Codable {
    var area: String
    var city: String
    var country: String
    
    private enum  CodingKeys: String, CodingKey {
        case area = "address_area"
        case city = "address_city"
        case country = "address_country"
    }
}

struct Spouse: Codable {
    var name: String
    var email: String
    
    private enum  CodingKeys: String, CodingKey {
        case name = "spouse_name"
        case email = "spouse_email"
    }
}

struct User: Codable {
    var id: Int
    var name:  String
    var email: String
    var address: Address
    var spouse: Spouse
    
    private enum CodingKeys: String, CodingKey {
        case id, name, email
        case user = "user"
        case address = "address_detail"
        case spouse = "spouse_detail"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let user = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        id = try user.decode(Int.self, forKey: .id)
        name = try user.decode(String.self, forKey: .name)
        email = try user.decode(String.self, forKey: .email)
        
        let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        address = try Address(from: data.superDecoder(forKey: .address))
        spouse = try Spouse(from: data.superDecoder(forKey: .spouse))
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        var user = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        try user.encode(id, forKey: .id)
        try user.encode(name, forKey: .name)
        try user.encode(email, forKey: .email)
    }
}

let json = """
{
    "error": 0,
    "message": "No error found",
    "user": {
        "id": 1,
        "name": "Didats Triadi",
        "email": "didats@somewhere.com",
        "address_detail": {
            "address_area": "Sawojajar, Kedungkandang",
            "address_city": "Malang",
            "address_country": "Indonesia"
        },
        "spouse_detail": {
            "spouse_name": "Isyana Sarasyati",
            "spouse_email": "isyana@somewhere.com"
        }
    }
}
"""

let jsonData = json.data(using: .utf8)
let obj = try! JSONDecoder().decode(User.self, from: jsonData!)
print("Obj: \(obj)")
Show Comments