Handling API's response hierarchy in Swift

Subscribe to my newsletter and never miss my upcoming articles

Hello my fellow sorcerers and witches, Leo here.

Today we'll explore some parsing magic that you can do to handle the way your API deal with json. It's a important topic because oftentimes your backend APIs will send you data that isn't much friendly to deal with, this leads to misunderstandings and error prone codes. The big picture here is to remove OR add complexity to our parsed json data as we wish.

Let's stop talking and do some magic decoder parsing.

The problem

You receive a json with X hierarchy and you want to parse it to another hierarchy structure.

First let us visualize the first json response we're getting from the server:

let houseJson = """
    {
        "height": 10,
        "width": 20,
        "color": "red",
        "address" : {
            "streetName": "Holy Street",
            "houseNumber": 42
        }
    }

""".data(using: .utf8)!

Notice that we have two hierarchy levels this json. The first with the keys height, width, color and address. And the second one with streetName and the houseNumber. Now let's suppose that your goal is to flatten this hierarchy, resulting this kind of structure:

let houseJson = """
    {
        "height": 10,
        "width": 20,
        "color": "red",
        "streetName": "Holy Street",
        "houseNumber": 42
    }

""".data(using: .utf8)!

Of course we'll take advantage of the Swift Decodable to handle this. We will create a custom decodable to do the heavy lifting for us:

struct House: Decodable { //1
    let height: Int
    let width: Int
    let color: String
    let streetName: String
    let houseNumber: Int

    enum CodingKeys: String, CodingKey {
        case height
        case width
        case color
        case address //2
    }

    enum AddressCodingKeys: String, CodingKey { //3
        case streetName
        case houseNumber
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self) //4
        height = try container.decode(Int.self, forKey: .height)
        width = try container.decode(Int.self, forKey: .width)
        color = try container.decode(String.self, forKey: .color)

        let houseContainer = try container.nestedContainer(keyedBy: AddressCodingKeys.self, forKey: .address) // 5

        streetName = try houseContainer.decode(String.self, forKey: .streetName)
        houseNumber = try houseContainer.decode(Int.self, forKey: .houseNumber)

    }
}

Let's analyze each data point. At 1 mark we conform the struct with Decodable protocol this ensures that we can use the struct to decode the json data and we already set the final properties that we want, therefore the struct won't has the Address object, it already has the flattened structure on his properties.

The 2 mark is important because we will use that to get link to AddressCodingKeys nested container that we'll check later. At 3 mark we are creating the nested CodingKey enum representing the nested hierarchy, this is important because when we build the init func we'll use it to parse the data.

In the init method is where the magic began and things get interesting. First on 4 mark we get the outer container and get the keys we want from it, in this example the height, width and color. And finally in the 5 mark we get the nested type container and we decode the last keys for the properties that we want.

It's pretty simple right?

And to test it you can use this in your playground:

let decoder = JSONDecoder()
let house = try! decoder.decode(House.self, from: houseJson)
print(house)

You will see this result:

Screen Shot 2021-02-04 at 08.46.54.png

Quick Tip

To better visualize objects hierarchy in the console recently I learned the dump function, that will improve the visualization to this:

let decoder = JSONDecoder()
let house = try! decoder.decode(House.self, from: houseJson)
dump(house)

Screen Shot 2021-02-04 at 08.49.03.png

Plot Twist

Imagine now that you want to do the inverse... You are receiving and flattened structure and you want to add some object hierarchy to it. Look the flattened data below:

let houseJson2 = """
    {
        "height": 10,
        "width": 20,
        "color": "red",
        "streetName": "Holy Street",
        "houseNumber": 42
    }

""".data(using: .utf8)!

And we want to transform into this structure:

let houseJson2 = """
    {
        "height": 10,
        "width": 20,
        "color": "red",
        "address" : {
            "streetName": "Holy Street",
            "houseNumber": 42
        }
    }

""".data(using: .utf8)!

The exact inverse operation, just to educational purposes. This are only examples, you can and should adapt for your APIs data types and structure though.

Moving on next, we need to create a new struct called Address to add the hierarchy to our data.

struct Address: Decodable {
    let streetName: String
    let houseNumber: Int
}

We don't need to add the custom coding keys to Address because the decodable protocol already knows how to parse this object, thank Swift for this. And finally now we can build the new House custom decodable structure:

struct House2: Decodable { let height: Int let width: Int let color: String let address: Address //1

enum CodingKeys: String, CodingKey { // 2
    case height
    case width
    case color
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    height = try container.decode(Int.self, forKey: .height)
    width = try container.decode(Int.self, forKey: .width)
    color = try container.decode(String.self, forKey: .color)

    address = try Address(from: decoder) //3
}

}

The 1 mark we are adding the Address property object to the House2 struct, here we sinalize to the compiler that we will eventually parse this object. The 2 mark don't include the Address key because it doesn't exists in the second example json, so we have nothing to try to decode. And the last mark we use the default init from Address that he inherited from Decodable protocol to parse the object for us.

And the final result you can check below:

let decoder2 = JSONDecoder()
let house2 = try! decoder.decode(House2.self, from: houseJson2)
dump(house2)

Screen Shot 2021-02-04 at 09.14.04.png

Conclusion

This is a brief intro to the powerful word of custom decodable world and how you can take benefits of it. If you want more complex coddle examples you can watch this talk where are many other very interesting features to learn.

And if you are reading until here, I need to say to you thank you very much and I'm glad this helps, specially me, understand the coding key decodable mechanics. Any comment or suggestions please share below.

Thanks for reading and... That's all folks.

credit: image

No Comments Yet