Swift Decode JSON with mixed types and possible sub structures

Viewed 730

I have to decode a given JSON structure coming from an API in Swift 4. The problem is that at some point in the tree I have at the same level different kind of datas and one of the type can have sub elements.

I've tried several JSONDecoder and Decodable techniques but so far I havn't find a solution.

Simplified JSON :

{
    "menuName": "Menu 1",
    "menuId": 1,
    "menuGroups": [
        {
            "type": "group",
            "name": "Group 1",
            "menuEntry": [
                {
                    "type": "group",
                    "name": "Sub Group 1.1",
                    "menuEntry": [
                        {
                            "type": "menuItem",
                            "productName": "Item 1",
                            "productPrice": "9.00"
                        },
                        {
                            "type": "menuItem",
                            "productName": "Item 2",
                            "productPrice": "12.00"
                        }
                    ]
                }, {
                    "type": "menuItem",
                    "productName": "Item 3",
                    "productPrice": "9.00"
                }
            ]
        }
    ]
}

Here are the decodables I'm trying to use :

struct Menu: Decodable {
    let menuName: String
    let menuId: Int
    let categories: [MenuCategory]

    enum CodingKeys : String, CodingKey {
        case menuName
        case menuId
        case categories = "menuGroups"
    }
}

struct MenuCategory: Decodable {
    let type: String
    let name: String
    let items: [CategoryItem]

    enum CodingKeys : String, CodingKey {
        case type
        case name
        case items = "menuEntry"
    }


}

enum CategoryItem: Decodable {
    case group(MenuCategory)
    case menuItem(MenuItem)

    public init(from decoder: Decoder) throws {

        let container = try decoder.singleValueContainer()
        do {
            let item = try container.decode(MenuCategory.self)
            self = .group(item)
            return
        } catch let err {
            print("error decoding category: \(err)")
        }

        do {
            let item = try container.decode(MenuItem.self)
            self = .menuItem(item)
            return
        } catch let err {
            print("error decoding item: \(err)")
        }
        try self.init(from: decoder)
    }
}

struct MenuItem: Decodable {
    let type: String
    let productName: String
    let productPrice: String

    enum CodingKeys : String, CodingKey {
        case type = "type"
        case productName
        case productPrice
    }
}

I think that using :

let container = try decoder.singleValueContainer()

is wrong because the container shouldn't be a single value container, but I don't know what to choose / do from here...

Would anyone have an idea on that ? How would you decode some JSON like in the example ?

2 Answers

You're very, very close, and have done a good job designing your data structures. You just need to try to decode each possible option in CategoryItem.

public init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    if let item = try? container.decode(MenuCategory.self) {
        self = .group(item)
    } else if let item = try? container.decode(MenuItem.self) {
        self = .menuItem(item)
    } else {
        throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath,
                                                debugDescription: "Not a group or item"))
    }
}

This container is a single value container because at this point in the decoding, you're decoding just one thing, a group or an item. It's up to each of those single values to handle their subcomponents.

use this stuct

struct YourStruct: Codable {
    let menuName: String
    let menuID: Int
    let menuGroups: [MenuGroup]

    enum CodingKeys: String, CodingKey {
        case menuName
        case menuID = "menuId"
        case menuGroups
    }
}

struct MenuGroup: Codable {
    let type, name: String
    let menuEntry: [MenuGroupMenuEntry]
}

struct MenuGroupMenuEntry: Codable {
    let type: String
    let name: String?
    let menuEntry: [MenuEntryMenuEntry]?
    let productName, productPrice: String?
}

struct MenuEntryMenuEntry: Codable {
    let type, productName, productPrice: String
}

and in datatask after cheking that you dont have an error

if let data = data {
                let decoder = JSONDecoder()
                guard let decodedJson = try? decoder.decode(YourStruct.self, from: data) else { completion(nil) ; return }

            }

hope this help

Related