How to dynamically add properties to a constant Decodable object in Swift?

Viewed 985

Background

Basically I got an api that returns something like this:

    "order_detail": [
        {
            "id": 6938,
            "order_id": 6404,
            "item_price": "4",
            ..
            "item": {
                "id": 12644,
                "ref": "Iced Caffe Americano",
                "short_description": "",
                ..

and in my decodable obj i got this

public struct OrderDetail: Decodable {
    public let id: Int
    public let order_id: Int
    public let item_price: String?
    ..
    public let item: Item?

and

public struct Item: Decodable {
    public var id: Int
    public var ref: String?
    public var short_description: String?

The problem is that somewhere else in the code, there is a method that's expecting the Item object to have item_price.

Question

What I want to do is swizzle or mutate this constant Item object and dynamically add item_price property to it.. How can I do that?

Workarounds, other solutions

1. Change json

I know there are many other solutions to this same problem (I'm working on it as we speak, which is simply modifying the api endpoint to suit my needs).. but again that option is not always possible (ie suppose the backend team is separate)

2. Change the function expectation

That is also possible, but also not inexpensive as this function is used in many other places in the app which I don't potentially have control over

2 Answers

If you want to add a property to a Decodable type that's not part of its JSON representation, so simply need to declare a CodingKey conformant type and leave out the specific property name so that the automatically synthesised init(from decoder:Decoder) initialiser will know not to look for that value in the JSON.

Btw you should also conform to the Swift naming convention (lowerCamelCase for variable names) and use CodingKey to map the JSON keys to the property names.

public struct Item: Decodable {
    public var id: Int
    public var ref: String?
    public var shortDescription: String?
    public var itemPrice: String? // or whatever else its type needs to be

    private enum CodingKeys: String, CodingKey {
        case id, ref, shortDescription = "short_description"
    }
}

This is one way to achieve this

Take over the initialization of Item in OrderDetail decoding.

struct OrderDetail: Decodable {
    let id: Int
    let orderId: Int
    let itemPrice: String?
    let item: Item

    private enum OrderDetailCodingKey: CodingKey {
        case id
        case orderId
        case itemPrice
        case item
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: OrderDetailCodingKey.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.orderId = try container.decode(Int.self, forKey: .orderId)

        let itemPrice = try container.decode(String?.self, forKey: .itemPrice)
        self.itemPrice = itemPrice
        self.item = try Item(from: decoder, itemPrice: itemPrice)
    }
}

Use a custom initializer to create your item.

struct Item: Decodable {
    let id: Int
    let ref: String?
    let shortDescription: String?
    let itemPrice: String?

    private enum ItemCodingKeys: CodingKey {
        case id
        case ref
        case shortDescription
    }

    init(from decoder: Decoder, itemPrice: String?) throws {
        let container = try decoder.container(keyedBy: ItemCodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.ref = try? container.decode(String.self, forKey: .ref)
        self.shortDescription = try? container.decode(String.self, forKey: .shortDescription)
        self.itemPrice = itemPrice
    }
}

You can call the following function to test the functionality:

private func test() {
        let json = """
            {"id":6938,"orderId":6404,"itemPrice":"4","item":{"id":12644,"ref":"Iced Caffe Americano","shortDescription":""}}
        """
        let data = json.data(using: .utf8)
        let decoder = JSONDecoder()
        if let data = data {
            do {
                let order = try decoder.decode(OrderDetail.self, from: data)
                print(order)
            } catch let jsonError {
                os_log("JSON decoding failed [%@]", String(describing: jsonError))
            }
        } else {
            os_log("No data found")
        }
    }
Related