Декодирование словаря в зависимости от ClassType

Допустим, у меня есть переменная 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
    }
  }
}

Сначала декодируйте clientType, а затем используйте переключатель этого значения для декодирования соответствующего подкласса клиента?

Joakim Danielson 10.04.2024 13:17

Я предполагаю, что в json между родительским и дочерним классами есть другие различия в полях данных? Как будто нет, вы могли бы декодировать их все как Client, а затем перебрать массив и преобразовать на основе clientType.

flanker 10.04.2024 13:53

Да, это кажется правильным подходом. Можете ли вы дать небольшое представление о том, как это должно выглядеть? Пробовал уже, но так, как я реализовал, не сработало.

rickmcrick 10.04.2024 13:54

@flanker да, есть и другие различия в полях данных, которые мне нужны

rickmcrick 10.04.2024 13:56

Предполагается, что да :) Подход @JoakimDanielson будет правильным. Однако в зависимости от того, как/где используется код и насколько велики различия между классами, быстрый, грязный и хакерский подход заключается в создании временного класса со всеми свойствами двух, с необязательными различиями, позволяющими Декодируется для выполнения работы, а затем отображается в окончательные структуры данных. Это не лучшая практика (поэтому не добавляю ее в качестве ответа!), но ее легко построить/настроить, если все, что вам нужно, это перенести данные в личный проект!

flanker 10.04.2024 14:07

@flanker, спасибо за твои идеи, но это касается деловой среды, поэтому я постараюсь сделать это правильно :D

rickmcrick 10.04.2024 15:03

@JoakimDanielson добавил json для большего контекста. проблема в том, что я не знаю, как декодировать clientType, потому что он находится внутри класса, который я хочу декодировать. Может быть, у вас есть идея?

rickmcrick 10.04.2024 15:25

Перечитывая вопрос, я вижу, что у вас есть только два типа: Client и SpecialClinet, поэтому я бы просто сначала попытался декодировать в один тип, а если это не удастся, декодировать в другой.

Joakim Danielson 10.04.2024 16:22

Если вы отвечаете за серверную часть, рассмотрите возможность отправки более подходящего, Codable-совместимого JSON, например, двух массивов, по одному для каждого типа клиента и с идентификатором clientId1111 в качестве свойства внутри типа, по крайней мере, что-то, что можно декодировать без реализации init(from:).

vadian 10.04.2024 16:35

Пример структуры json недействителен, поэтому он не помогает найти ответ. Сочетание массивов/словарей не имеет смысла с точки зрения вопроса. Можете ли вы опубликовать (очищенный) фактический JSON?

flanker 10.04.2024 18:56
Как сделать HTTP-запрос в Javascript?
Как сделать HTTP-запрос в Javascript?
В JavaScript вы можете сделать HTTP-запрос, используя объект XMLHttpRequest или более новый API fetch. Вот пример для обоих методов:
0
10
101
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

РЕДАКТИРОВАНИЕ ОП прояснило структуру 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 11.04.2024 15:12

@rickmcrick Я не мог; разберитесь в структуре вашего примера (это недопустимый JSON), поэтому, если вы можете обновить вопрос реальным фрагментом, я готов обновить ответ.

flanker 11.04.2024 16:45

обновил вопрос действительным json @flanker

rickmcrick 12.04.2024 09:05

обновил ответ на исправленный json @rickmcrick

flanker 14.04.2024 21:29

большое спасибо! этот подход намного чище, чем тот, который у меня был раньше. Продолжайте хорошую работу!

rickmcrick 15.04.2024 09:47

Другие вопросы по теме