swift: Как я могу декодировать массив объектов json, не создавая структуру, которая содержит массив указанных объектов?

Мои данные выглядят так:

"places": [
        {
            "id": 15,
            "name": "København",
            "typeId": 6,
            "coordinates": {
                "lat": "55.6760968",
                "lng": "12.5683372"
            },
            "count": 2779
        },
        {
            "id": 19,
            "name": "København S",
            "typeId": 3,
            "coordinates": {
                "lat": "55.6508754",
                "lng": "12.5991891"
            },
            "count": 1168
        }
]

Я хочу избежать этого:

struct Places: Decodable {
    let places: [Place]
}

Что предлагает QuickType.io: https://app.quicktype.io?share=j22hopu А вместо этого просто декодируйте в списке мест. Так что это сработает:

let places = try JSONDecoder().decode([Place].self, from: data)

Возможные решения, которые я нашел до сих пор:

  1. Решение "потертости": https://stackoverflow.com/a/62403633/13481876

  2. Создайте общую структуру декодируемого массива: https://swiftsenpai.com/swift/decode-dynamic-keys-json/


person Alexander Flensborg    schedule 26.11.2020    source источник
comment
Нет, не можешь. Так сказать нужно начинать сверху. Но почему это проблема, вы можете легко получить доступ и сохранить только массив Place?   -  person Joakim Danielson    schedule 26.11.2020
comment
@JoakimDanielson Мне просто некрасиво иметь структуру только для того, чтобы правильно декодировать мой json. Я боялся, что это так.   -  person Alexander Flensborg    schedule 27.11.2020
comment
Я начал с использования ответа Cristik, однако по мере того, как я продолжал реализовывать остальную часть моего webRepo для API, я обнаружил, что вышеупомянутый случай является пограничным случаем и не применим ни к чему, кроме этого одного вызова структуры / API. Поэтому я закончил тем, что использовал метод, упомянутый gcharita, который декодирует мой объект в [String: [OBJECT]]. В целом, я думаю, что метод Cristik более элегантен, и это решение, которое я бы использовал, если бы у меня была необходимость, больше, чем несколько случаев. Поскольку мое мышление было в менее загроможденном пространстве имен, я выбрал gcharita.   -  person Alexander Flensborg    schedule 10.12.2020


Ответы (2)


Если вам это нужно несколько раз, вы можете создать свою собственную общую структуру, которая декодирует любой найденный ключ:

struct Nester<T: Decodable>: Decodable {
    let elements: [T]
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let key = container.allKeys.first {
            elements = try container.decode([T].self, forKey: key)
        } else {
            // we run into an empty dictionary, let's signal this
            throw DecodingError.typeMismatch([String:Any].self, DecodingError.Context(codingPath: [], debugDescription: "Expected to find at least one key"))
        }
    }
    
    // A coding key that accepts whatever string value it is given
    struct CodingKeys: CodingKey {
        let stringValue: String
        var intValue: Int? { nil }
        
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
                
        init?(intValue: Int) { return nil }
    }
}

Имея это под рукой, вы можете расширить JSONDecoder, чтобы получить более удобный сайт для звонков:

extension JSONDecoder {
    func decode<T: Decodable>(nested: [T].Type, from data: Data) throws -> [T] {
        try decode(Nester<T>.self, from: data).elements
    }
}

Тогда остается просто вызвать новую перегрузку:

let places = try JSONDecoder().decode(nested: [Place].self, from: data)

P.S. при желании вы можете скрыть сложную структуру внутри расширения, в результате получится что-то вроде этого:

extension JSONDecoder {
    func decode<T: Decodable>(nested: [T].Type, from data: Data) throws -> [T] {
        try decode(Nester<T>.self, from: data).elements
    }
    
    private struct Nester<T: Decodable>: Decodable {
        let elements: [T]
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            if let key = container.allKeys.first {
                elements = try container.decode([T].self, forKey: key)
            } else {
                throw DecodingError.typeMismatch([String:Any].self, DecodingError.Context(codingPath: [], debugDescription: "Expected to find at least one key"))
            }
        }
        
        struct CodingKeys: CodingKey {
            let stringValue: String
            var intValue: Int? { nil }
            
            init?(stringValue: String) {
                self.stringValue = stringValue
            }
                    
            init?(intValue: Int) { return nil }
        }
    }
}

Обратной стороной является то, что вы не сможете повторно использовать структуру, если хотите расширить другие декодеры, кроме JSON.

person Cristik    schedule 27.11.2020
comment
Идея расширения JSONDecoder хороша для повторного использования. Объединение двух ответов даст интересный результат :) - person gcharita; 27.11.2020

Существует возможный способ избежать структуры верхнего уровня (Places) путем декодирования вашего JSON в тип [String: [Place]], а затем получения первого элемента свойства Dictionary values:

let decoder = JSONDecoder()
do {
    let places = try decoder.decode([String: [Place]].self, from: data)
    print(places.values.first ?? [])
} catch {
    print(error)
}
person gcharita    schedule 27.11.2020
comment
Более чистое, чем мое решение, которое добавляет накладные расходы на новую структуру. :пальцы вверх: - person Cristik; 27.11.2020