Допустим, у меня есть переменная clients: [String: Client]
. Клиент — это мой родительский класс, у которого есть подкласс SpecialClient
. Оба класса являются кодируемыми, и я хочу декодировать строку JSON в словарь. В этом словаре я хочу иметь оба типа клиентов.
Для этого у каждого клиента в строке JSON есть переменная clientType
, которая определяет, является ли он SpecialClient
или нет. Теперь мне нужно прочитать это значение и на его основе добавить SpecialClient
или обычный Client
в словарь.
В настоящее время у меня есть что-то вроде этого, что, очевидно, просто создает клиентов в словаре. Я провел некоторые исследования, но не смог найти способ.
class Object: Codable {
var clients: [String:Client]
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.clients = try container.decode([String : Client].self, forKey: .clients)
}
}
Иметь контейнер с клиентами из классов Client и SpecialClient.
Для большего контекста: JSON выглядит так:
{
"groups": {
"GroupId1": {
"label": "group1",
"id": "GroupId1"
},
"GroupId2": {
"label": "group2",
"id": "GroupId2"
}
},
"clients": {
"ClientId1": {
"clientType": "APP",
"label": "client1"
},
"ClientId2": {
"clientType": "SPECIAL",
"label": "Client2",
"specialValue": 2
}
}
}
Я предполагаю, что в json между родительским и дочерним классами есть другие различия в полях данных? Как будто нет, вы могли бы декодировать их все как Client, а затем перебрать массив и преобразовать на основе clientType
.
Да, это кажется правильным подходом. Можете ли вы дать небольшое представление о том, как это должно выглядеть? Пробовал уже, но так, как я реализовал, не сработало.
@flanker да, есть и другие различия в полях данных, которые мне нужны
Предполагается, что да :) Подход @JoakimDanielson будет правильным. Однако в зависимости от того, как/где используется код и насколько велики различия между классами, быстрый, грязный и хакерский подход заключается в создании временного класса со всеми свойствами двух, с необязательными различиями, позволяющими Декодируется для выполнения работы, а затем отображается в окончательные структуры данных. Это не лучшая практика (поэтому не добавляю ее в качестве ответа!), но ее легко построить/настроить, если все, что вам нужно, это перенести данные в личный проект!
@flanker, спасибо за твои идеи, но это касается деловой среды, поэтому я постараюсь сделать это правильно :D
@JoakimDanielson добавил json для большего контекста. проблема в том, что я не знаю, как декодировать clientType, потому что он находится внутри класса, который я хочу декодировать. Может быть, у вас есть идея?
Перечитывая вопрос, я вижу, что у вас есть только два типа: Client и SpecialClinet, поэтому я бы просто сначала попытался декодировать в один тип, а если это не удастся, декодировать в другой.
Если вы отвечаете за серверную часть, рассмотрите возможность отправки более подходящего, Codable
-совместимого JSON, например, двух массивов, по одному для каждого типа клиента и с идентификатором clientId1111
в качестве свойства внутри типа, по крайней мере, что-то, что можно декодировать без реализации init(from:)
.
Пример структуры json недействителен, поэтому он не помогает найти ответ. Сочетание массивов/словарей не имеет смысла с точки зрения вопроса. Можете ли вы опубликовать (очищенный) фактический JSON?
РЕДАКТИРОВАНИЕ ОП прояснило структуру JSON - это один гетерогенный словарь, а не массив из нескольких однородных словарей - и первоначальный подход больше не подходит. Однако оставим ответ здесь, поскольку он может оказаться полезным для будущих поисков. Пересмотренный ответ находится в конце.
В этом ответе сделано предположение из-за отсутствия ясности в вопросе о структуре JSON. Я предположил, что структура:
{
"clients": [
{
"clientId1111": {
"clientType": "BASIC_TYPE",
"label": "Test1"
}
},
//...
{
"clientId1114": {
"clientType": "SPECIAL_TYPE",
"label": "Test4",
"specialCode": 4
}
}
]
}
т. е. массив словарей, на верхнем уровне с ключом clients
. Если это не так, вам может потребоваться адаптировать ответ.
На этой основе я создал ряд моделей:
Базовая Client
модель. Это соответствует стандарту Decodeable, поэтому никакого специального декодирования не требуется.
class Client: Decodable {
let label: String
let clientType: String
init(label: String, clientType: String) {
self.label = label
self.clientType = clientType
}
}
Модель SpecialClient
:
class SpecialClient: Client {
let specialCode: Int //just to differentiate from Client
enum CodingKeys: CodingKey {
case label, clientType, specialCode
}
required init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
specialCode = try container.decode(Int.self, forKey: .specialCode)
let label = try container.decode(String.self, forKey: .label)
let type = try container.decode(String.self, forKey: .clientType)
super.init(label: label, clientType: type)
}
}
При этом используется собственный декодер и вызывается super.init
с полями данных. [Я чувствую, что есть лучший способ справиться с этим, но я так давно не использовал наследование с коддлом...!]
а затем основная часть декодирования происходит в модели Object
class Object: Decodable {
var clients: [String:Client]
enum CodingKeys: CodingKey {
case clients
}
required init(from decoder: any Decoder) throws {
var imports = [ [String: Client] ]()
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
var dataContainer = try container.nestedUnkeyedContainer(forKey: .clients)
while !dataContainer.isAtEnd {
do {
let imported = try dataContainer.decode([String: SpecialClient].self)
imports.append(imported)
} catch {
let imported = try dataContainer.decode([String: Client].self)
imports.append(imported)
}
}
} catch {
fatalError(error.localizedDescription)
}
clients = imports.reduce(into:[String: Client]()){ master, childDict in
childDict.forEach{ childPair in
master[childPair.key] = childPair.value
}
}
}
}
здесь он использует вложенный контейнер без ключа, поэтому вы можете получить доступ как к ключу словаря (clientId###
), так и к его значению (Client
или SpecialClient
).
Метод перебирает каждую запись в контейнере без ключа и для каждой сначала пытается декодировать более специализированный дочерний класс, а затем, если это не удается, пытается декодировать более простой родительский класс в блоке catch. Декодированная пара значений, то есть словарь [String: Client/SpecialClient]
, добавляется во временный массив.
Наконец, он уменьшил (сгладил) временный массив словарей в основной clients
словарь.
Работая над своим тестовым json, object.clients.forEach{print($0)}
выдает следующий результат:
(key: "clientId1114", value: __lldb_expr_18.SpecialClient)
(key: "clientId1112", value: __lldb_expr_18.Client)
(key: "clientId1111", value: __lldb_expr_18.Client)
(key: "clientId1113", value: __lldb_expr_18.SpecialClient)
как и ожидалось.
На практике, если бы я реализовал это, я бы использовал структуры для типов клиентов, а не классов, и спрятал бы их за протоколом словаря. Это упростило бы часть декодирования, поскольку нет необходимости иметь дело с такими вещами, как методы super.init.
Чтобы декодировать словарь, имеющий множество типов, необходимо найти единую структуру, которая может представлять все из них, поскольку это позволит Decodable
обрабатывать словарь как единый объект. Альтернативой была бы обработка почти построчно, возможно, с JSONSerialization
, что было бы тяжело. Лучше позволить Decodable
сделать тяжелую работу.
Способ решения этой проблемы — использовать перечисление со связанными значениями с регистром для каждого из возможных типов в словаре. Для этого потребуется кастом init(from:)
. Для поддержки реалистичного JSON, который теперь добавлен к вопросу, жизнеспособным решением является следующий подход...
enum AnyClient: Decodable {
case standard(Client)
case special(SpecialClient)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let client = try container.decode(SpecialClient.self)
self = .special(client)
} catch DecodingError.keyNotFound {
let client = try container.decode(Client.self)
self = .standard(client)
}
}
}
Как только вы это сделаете, остальная часть декодирования станет стандартной Dedcodable
, хотя и с небольшими накладными расходами на поддержку наследования в Client
/SpecialClient
классах.
class Client: Decodable {
let label: String
let clientType: String
init(label: String, clientType: String) {
self.label = label
self.clientType = clientType
}
}
class SpecialClient: Client {
let specialValue: Int //just to differentiate from Client
enum CodingKeys: CodingKey {
case label, clientType, specialValue
}
required init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
specialValue = try container.decode(Int.self, forKey: .specialValue)
let label = try container.decode(String.self, forKey: .label)
let type = try container.decode(String.self, forKey: .clientType)
super.init(label: label, clientType: type)
}
}
class Group: Decodable {
let label: String
let id: String
}
class Object: Decodable {
var groups: [String: Group]
var clients: [String: AnyClient]
}
На этом этапе вы расшифровали json. Значения клиентского словаря теперь заключены в перечисление; это может быть полезно или мешать продвижению вперед в зависимости от того, как вы планируете использовать их где-либо еще, но было бы легко перебрать словарь и преобразовать его в [String: Client]
. Это можно даже сделать внутри кастома .init(from:)
в Object
. Суш как
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.groups = try container.decode([String : Group].self, forKey: .groups)
let anyClients = try container.decode([String : AnyClient].self, forKey: .clients)
clients = anyClients.reduce(into: [String: Client]()) { dict, item in
switch item.value {
case let .standard(client): dict[item.key] = client
case let .special(client): dict[item.key] = client
}
}
}
передавая предоставленный json через:
let data = json.data(using: .utf8)!
let object = try JSONDecoder().decode(Object.self, from: data)
object.groups.forEach{print($0)}
object.clients.forEach{print($0)}
обеспечивает ожидаемый результат
(key: "GroupId1", value: __lldb_expr_161.Group)
(key: "GroupId2", value: __lldb_expr_161.Group)
(key: "ClientId1", value: __lldb_expr_161.AnyClient.standard(__lldb_expr_161.Client))
(key: "ClientId2", value: __lldb_expr_161.AnyClient.special(__lldb_expr_161.SpecialClient))
Привет, во-первых, ваш ответ кажется правильным, как это сделать. Так что спасибо тебе. У меня проблема в том, что json, который я получаю из серверной части, не соответствует той структуре, которую вы предполагали (больше похоже на ту, которую я описал), и поэтому .nestedUnkeyedContainer не возвращает массив. Он действительно ничего не возвращает, потому что читает текст из json, чего не ожидает, и ловит ошибку. Мне нужно придумать, как это сделать с помощью моей структуры json, что кажется не таким уж простым. Но теперь я лучше понимаю, в чем заключаются мои проблемы, поэтому еще раз спасибо!
@rickmcrick Я не мог; разберитесь в структуре вашего примера (это недопустимый JSON), поэтому, если вы можете обновить вопрос реальным фрагментом, я готов обновить ответ.
обновил вопрос действительным json @flanker
обновил ответ на исправленный json @rickmcrick
большое спасибо! этот подход намного чище, чем тот, который у меня был раньше. Продолжайте хорошую работу!
Сначала декодируйте clientType, а затем используйте переключатель этого значения для декодирования соответствующего подкласса клиента?