Быстрое декодирование JSON со смешанными типами и возможными подструктурами

Мне нужно декодировать заданную структуру JSON, поступающую из API в Swift 4. Проблема в том, что в какой-то момент дерева у меня есть на одном уровне разные типы данных, и один из типов может иметь подэлементы.

Я пробовал несколько методов JSONDecoder и Decodable, но пока не нашел решения.

Упрощенный 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"
                }
            ]
        }
    ]
}

Вот декодируемые данные, которые я пытаюсь использовать:

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
    }
}

Я думаю, что с помощью:

let container = try decoder.singleValueContainer()

неправильно, потому что контейнер не должен быть контейнером с одним значением, но я не знаю, что выбрать/сделать отсюда...

У кого-нибудь есть идеи по этому поводу? Как бы вы раскодировали JSON, как в примере?


person ygosteli    schedule 16.01.2019    source источник


Ответы (2)


использовать эту статью

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
}

и в datatask после проверки, что у вас нет ошибки

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

            }

надеюсь, что это поможет

person teodik abrami    schedule 16.01.2019
comment
Спасибо, это действительно работает, по крайней мере, на тестовом наборе, теперь я попробую на полном наборе данных. - person ygosteli; 17.01.2019

Вы очень, очень близки к этому и проделали хорошую работу по проектированию своих структур данных. Вам просто нужно попытаться расшифровать каждый возможный вариант в 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"))
    }
}

Этот контейнер является контейнером с одним значением, потому что на данном этапе декодирования вы декодируете только одну вещь, группу или элемент. Каждое из этих отдельных значений должно обрабатывать свои подкомпоненты.

person Rob Napier    schedule 16.01.2019
comment
Спасибо за ответ! При этом у меня есть EXC_BAD_ACCESS на линии: if let item = try? container.decode(MenuCategory.self) { - person ygosteli; 16.01.2019
comment
Я не могу воспроизвести это в раскадровке, используя предоставленный вами JSON. Вам нужно будет углубиться в трассировку стека. Подобный сбой часто предполагает, возможно, несвязанную ошибку управления памятью где-то в коде. - person Rob Napier; 16.01.2019
comment
Немного поиграв с do/catch, я могу получить следующую ошибку: error : keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "menuEntryGroups", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "menuEntry", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil)) - person ygosteli; 16.01.2019
comment
Похоже, вам не хватает поля name в menuEntryGroups/Index 0/menuEntry/Index 0 (которое не соответствует JSON, который вы разместили выше). В любом случае, он говорит вам, где именно проблема. - person Rob Napier; 16.01.2019
comment
После более длительного просмотра ошибок и сравнения с json кажется, что он не меняется между типами Group и Item, поэтому он пытается получить свойство name для элемента, который вместо этого имеет productName. Я буду исследовать больше, почему это не меняется между ними. Спасибо - person ygosteli; 17.01.2019