Swift4 Decodable - декодировать половину ключей как словарь

У меня есть ситуация, когда сервер отправляет мне модель, в которой я знаю типы и имена некоторых ключей, а не другие. Однако пользователь может редактировать эти другие пары ключ-значение по своему усмотрению.

Пример:

{ "a": "B",
  "b": 42,
  "__customKey1": "customVal1",
  "__customKey2": [41, 42],
  "__customKey3": {"z":"x"}
}

Итак, в итоге я хочу получить модель с некоторыми объявленными свойствами и некоторыми значениями, помещенными в Dictionary<String, Any>, например.

struct MyStruct {
  var a: String?
  var b: Int?
  var dict: Dictionary<String,Any>
}

Я пробовал что-то вроде:

  public struct CodingKeysX: CodingKey {
      public var intValue: Int?
      public var stringValue: String

      public init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
      public init?(stringValue: String) { self.stringValue = stringValue }

      static func make(key: String) -> CodingKeysX {
          return CodingKeysX(stringValue: key)!
      }
  }

 init(from decoder: Decoder) throws {

        let co = try! decoder.container(keyedBy: CodingKeysX.self)

        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.a = try container.decode(String?.self, forKey: .a)
        self.b = try container.decode(Int?.self, forKey: .b)
        let allDeclaredKeys = container.allKeys.map({ $0.stringValue })
        self.dict = Dictionary<String, Any>()
        for key in co.allKeys.filter({ !allDeclaredKeys.contains($0.stringValue) }) {
            self.dict[key.stringValue] = try? co.decodeIfPresent(Any.self, forKey: key)
        }
    }

Но я получаю следующую ошибку времени компиляции:

Тип протокола «Любой» не может соответствовать «Декодируемому», поскольку только конкретные типы могут соответствовать протоколам.

Также кажется, что с помощью JSONDecoder я не могу получить ссылку на оригинал Data нам NSJSONSerialization. Так что я мог бы, я полагаю, сделать это наоборот, когда я сначала инициализирую dict, используя более старую технику, затем инициализирую модель, используя JSONDecoder, и заменяю init чем-то, что передает данные, но это просто кажется неправильным, потому что мы были бы эффективно десериализация дважды :/


person zaitsman    schedule 05.08.2019    source источник
comment
Я бы отказался и просто использовал JSONSerialization. Этот JSON неструктурирован; ты облит.   -  person matt    schedule 05.08.2019


Ответы (1)


Я не согласен с тем, что вы должны анализировать это как [String: Any]. В JSON существует всего несколько допустимых типов значений. Это далеко не Any.

Вместо этого вашей отправной точкой будет универсальный декодер JSON. Это декодируется в перечисление JSON.

let value = try JSONDecoder().decode(JSON.self, from: json)
==> Optional(["__customKey1": "customVal1", "b": 42, "a": "B", "__customKey2": [41, 42], "__customKey3": ["z": "x"]])

value["a"]?.stringValue   // Note value is a JSON, not a Dictionary
==> Optional("B")

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

struct MyStruct {
    var a: String?
    var b: Int?
    var dict: [String: JSON] // JSON values, not Any values
}

extension MyStruct: Decodable {

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: JSON.Key.self)

        let knownKeys = [JSON.Key("a"), JSON.Key("b")]

        // Unload the known keys. There's no need for these to be Optional unless
        // they really are optional (and nil is different than ""). The point,
        // is you can do any "normal" validation you want here and throw on error.
        self.a = try container.decodeIfPresent(String.self, forKey: JSON.Key("a"))
        self.b = try container.decodeIfPresent(Int.self, forKey: JSON.Key("b"))

        // Unload the rest into your dictionary
        self.dict = [:]
        for key in container.allKeys where !knownKeys.contains(key) {
            self.dict[key.stringValue] = try container.decode(JSON.self, forKey: key)
        }
    }
}

let ms = try JSONDecoder().decode(MyStruct.self, from: json)
=> MyStruct(a: Optional("B"), b: Optional(42), 
            dict: ["__customKey1": "customVal1", 
                   "__customKey2": [41, 42], 
                   "__customKey3": {"z": "x"}])

ms.dict["__customKey1"]?.stringValue  // Optional("customVal1")
person Rob Napier    schedule 05.08.2019
comment
это отличное решение, и я наткнулся на это в своем поиске. К сожалению, моя проблема в том, что SDK, который я должен использовать, требует Dictionary<String, Any> на другом конце. Вот почему я все равно хотел хранить в нем свои данные. Если я использую перечисление .JSON(), то мне нужно: а) преобразовать данные из JSON в эту модель б) преобразовать данные в Dictionary‹String, Any› для хранения c) прочитать, преобразовать данные из Dictionary‹String, Any› в это модель снова г) чтобы отправить обратно, преобразовать данные в JSON. Искренне - учитывая вышеизложенное, вы все равно решили бы следовать предложенному вами ответу? - person zaitsman; 05.08.2019
comment
Абсолютно. Преобразование тривиально (.dictionaryValue и init(Any)). Даже если бы я собирался создать собственный кодировщик/декодер, я бы использовал это перечисление JSON для выполнения всей работы внутри encode(to:) и init(with:Coder). - person Rob Napier; 05.08.2019