Swift 4 Decodable с ключами, не известными до времени декодирования
Как протокол Swift 4 Decodable справляется со словарем, содержащим ключ, имя которого неизвестно до выполнения? Например:
[
{
"categoryName": "Trending",
"Trending": [
{
"category": "Trending",
"trailerPrice": "",
"isFavourit": null,
"isWatchlist": null
}
]
},
{
"categoryName": "Comedy",
"Comedy": [
{
"category": "Comedy",
"trailerPrice": "",
"isFavourit": null,
"isWatchlist": null
}
]
}
]
Здесь у нас есть набор словарей; первая имеет клавиши categoryName
и Trending
, а вторая имеет клавиши categoryName
и Comedy
. Значение ключа categoryName
указывает мне имя второго ключа. Как выразить это с помощью Decodable?
Ответы
Ответ 1
Ключ в том, как вы определяете свойство CodingKeys
. Хотя обычно это enum
, это может быть все, что соответствует протоколу CodingKey
. А для создания динамических клавиш вы можете вызвать статическую функцию:
struct Category: Decodable {
struct Detail: Decodable {
var category: String
var trailerPrice: String
var isFavorite: Bool?
var isWatchlist: Bool?
}
var name: String
var detail: Detail
struct CodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
static let name = CodingKeys(stringValue: "categoryName")!
static func makeKey(name: String) -> CodingKeys {
return CodingKeys(stringValue: name)!
}
}
init(from coder: Decoder) throws {
let container = try coder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.detail = try container.decode([Detail].self, forKey: .makeKey(name: name)).first!
}
}
Использование:
let jsonData = """
[
{
"categoryName": "Trending",
"Trending": [
{
"category": "Trending",
"trailerPrice": "",
"isFavourite": null,
"isWatchlist": null
}
]
},
{
"categoryName": "Comedy",
"Comedy": [
{
"category": "Comedy",
"trailerPrice": "",
"isFavourite": null,
"isWatchlist": null
}
]
}
]
""".data(using: .utf8)!
let categories = try! JSONDecoder().decode([Category].self, from: jsonData)
(я изменил isFavourit
в JSON на isFavourite
, так как я думал, что это неправильное обращение. Это достаточно легко адаптировать код, если это не так)
Ответ 2
Вы можете написать пользовательскую структуру, которая функционирует как объект CodingKeys, и инициализировать ее строкой, чтобы она извлекала указанный вами ключ:
private struct CK : CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
Таким образом, как только вы знаете, что такое желаемый ключ, вы можете сказать (в init(from:)
override:
let key = // whatever the key name turns out to be
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
Итак, что я закончил делать, это сделать два контейнера из декодера - один с использованием стандартного перечисления CodingKeys для извлечения значения ключа "categoryName"
, а другой - с помощью CK struct для извлечения значения ключа, имя которого мы просто узнал:
init(from decoder: Decoder) throws {
let con = try! decoder.container(keyedBy: CodingKeys.self)
self.categoryName = try! con.decode(String.self, forKey:.categoryName)
let key = self.categoryName
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
}
В этом случае моя вся Декодируемая структура:
struct ResponseData : Codable {
let categoryName : String
let unknown : [Inner]
struct Inner : Codable {
let category : String
let trailerPrice : String
let isFavourit : String?
let isWatchList : String?
}
private enum CodingKeys : String, CodingKey {
case categoryName
}
private struct CK : CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
init(from decoder: Decoder) throws {
let con = try! decoder.container(keyedBy: CodingKeys.self)
self.categoryName = try! con.decode(String.self, forKey:.categoryName)
let key = self.categoryName
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
}
}
И вот тестовая кровать:
let json = """
[
{
"categoryName": "Trending",
"Trending": [
{
"category": "Trending",
"trailerPrice": "",
"isFavourit": null,
"isWatchlist": null
}
]
},
{
"categoryName": "Comedy",
"Comedy": [
{
"category": "Comedy",
"trailerPrice": "",
"isFavourit": null,
"isWatchlist": null
}
]
}
]
"""
let myjson = try! JSONDecoder().decode(
[ResponseData].self,
from: json.data(using: .utf8)!)
print(myjson)
И вот вывод инструкции print, доказывающий, что мы правильно заполнили наши структуры:
[JustPlaying.ResponseData(
categoryName: "Trending",
unknown: [JustPlaying.ResponseData.Inner(
category: "Trending",
trailerPrice: "",
isFavourit: nil,
isWatchList: nil)]),
JustPlaying.ResponseData(
categoryName: "Comedy",
unknown: [JustPlaying.ResponseData.Inner(
category: "Comedy",
trailerPrice: "",
isFavourit: nil,
isWatchList: nil)])
]
Конечно, в реальной жизни у нас будет некоторая обработка ошибок, без сомнения!
EDIT Позже я понял (частично благодаря ответу CodeDifferent), что мне не нужны два контейнера; Я могу исключить перечисление CodingKeys, и моя CK-структура может выполнить всю работу! Это ключевой разработчик общего назначения:
init(from decoder: Decoder) throws {
let con = try! decoder.container(keyedBy: CK.self)
self.categoryName = try! con.decode(String.self, forKey:CK(stringValue:"categoryName")!)
let key = self.categoryName
self.unknown = try! con.decode([Inner].self, forKey: CK(stringValue:key)!)
}
Ответ 3
тоже задал этот вопрос. Вот что в итоге придумал этот json:
let json = """
{
"BTC_BCN":{
"last":"0.00000057",
"percentChange":"0.03636363",
"baseVolume":"47.08463318"
},
"BTC_BELA":{
"last":"0.00001281",
"percentChange":"0.07376362",
"baseVolume":"5.46595029"
}
}
""".data(using: .utf8)!
Мы делаем такую структуру:
struct Pair {
let name: String
let details: Details
struct Details: Codable {
let last, percentChange, baseVolume: String
}
}
После декодирования:
if let pairsDictionary = try? JSONDecoder().decode([String: Pair.Details].self, from: json) {
var pairs: [Pair] = []
for (name, details) in pairsDictionary {
let pair = Pair(name: name, details: details)
pairs.append(pair)
}
print(pairs)
}
Также можно вызвать не пар.details.baseVolume, но pair.baseVolume:
struct Pair {
......
var baseVolume: String { return details.baseVolume }
......
Или напишите пользовательский init:
struct Pair {
.....
let baseVolume: String
init(name: String, details: Details) {
self.baseVolume = details.baseVolume
......