Custom Codable initializer

Recently I had to work with a rather strange json payload. The strange part was it had on string property which in fact was another json payload but as a string, inside two double-quotation, something like this:

{
    "name": "John Smith",
    "sku": "20223",
    "price": 23.95,
    "shipTo": {
        "name": "Jane Smith",
        "address": "123 Maple Street",
        "city": "Pretendville",
        "state": "NY",
        "zip": "12345"
    },
    "billTo": "{\"name\":\"John Smith\",\"address\":\"123MapleStreet\",\"city\":\"Pretendville\",\"state\":\"NY\",\"zip\":\"12345\"}"
}

Look at billTo. weird, right? There are two options in dealing with such a situation, easier one is decoding it as it is, then creating another small decoder for the string part. The other option is customizing the your codable strut to decode it there. First I went with the easy solution then I changed to the other method. To go with the first method you would have this model:

struct SamplePayload: Codable {   
    let name, sku: String?
    let price: Double?
    let shipTo: ShipTo?
    let billTo: String?
    
    struct ShipTo: Codable {
        let name, address, city, state: String?
        let zip: String?
    }
}

Then you would have something like this to decode it:

// Declaring BillTo struct:
struct BillTo: Codable {
    let name, address, city, state, zip: String?
}

// Code
let decoder = JSONDecoder()
// let data = read payload in Data format
do {
    let decoded = try decoder.decode(SamplePayload.self, from: data)
    if let billToString = decoded.billTo, let billToData = billToString.data(using: .utf8) {
        let decodedBillTo = try? decoder.decode(BillTo.self, from: data)
        print(decodedBillTo?.city)
    }
} catch {
    print("Failed to decode JSON")
}

// Output: 
// Optional("Pretendville")

It’s not a nice looking code, is it? Codable provides an initializer we can override and create our custom decoder. Using a KeyedDecodingContainer provided in the initizeir’s input arguments we can perform our decoding.

struct SamplePayload: Codable {   
    let name, sku: String?
    let price: Double?
    let shipTo: ShipTo?
    let billTo: BillTo?
    
    struct ShipTo: Codable {
        let name, address, city, state: String?
        let zip: String?
    }
    
    struct BillTo: Codable {
        let name, address, city, state, zip: String?
    }
    
    init(from decoder: Decoder) throws {
        // let container: KeyedDecodingContainer<SamplePayload.CodingKeys> = try decoder.container(keyedBy: SamplePayload.CodingKeys.self)
        let container = try decoder.container(keyedBy: SamplePayload.CodingKeys.self)
        self.name = try container.decodeIfPresent(String.self, forKey: SamplePayload.CodingKeys.name)
        self.sku = try container.decodeIfPresent(String.self, forKey: SamplePayload.CodingKeys.sku)
        self.price = try container.decodeIfPresent(Double.self, forKey: SamplePayload.CodingKeys.price)
        self.shipTo = try container.decodeIfPresent(ShipTo.self, forKey: SamplePayload.CodingKeys.shipTo)
        let billToString = try container.decodeIfPresent(String.self, forKey: SamplePayload.CodingKeys.billTo)
        let jsonDecoder = JSONDecoder()
        if let data = billToString?.data(using: .utf8), let billToDecoded = try? jsonDecoder.decode(BillTo.self, from: data) {
            self.billTo = billToDecoded
        } else {
            self.billTo = nil
        }
    }
}

According to Apple, a KeyedDecodingContainer is:

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

Here we do almost the same thing as we did earlier, but in an elegant way. Everything is moved inside the struct and the user is spared a couple of if let blocks.

Download the Playgoround files from here https://github.com/maysamsh/swift-custom-codable

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.