Swift Codable — синтаксический анализ массива JSON, который может содержать разные типы данных

Я пытаюсь разобрать массив JSON, который может быть

{
  "config_data": [
      {
        "name": "illuminate",
        "config_title": "Blink"
      },
      {
        "name": "shoot",
        "config_title": "Fire"
      }
    ]
}

или он может быть следующего типа

{
  "config_data": [
          "illuminate",
          "shoot"
        ]
}

или даже

{
    "config_data": [
              25,
              100
            ]
  }

Итак, чтобы разобрать это с помощью JSONDecoder, я создал следующую структуру:

Struct Model: Codable {
  var config_data: [Any]?

  enum CodingKeys: String, CodingKey {
    case config_data = "config_data"
   }

  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    config_data = try values.decode([Any].self, forKey: .config_data)
  }
}

Но это не сработает, так как Any не подтверждает декодируемый протокол. Какое может быть решение для этого. Массив может содержать любые данные


person Ganesh Somani    schedule 18.01.2018    source источник


Ответы (3)


Я использовал quicktype, чтобы вывести тип config_data, и он предложил перечисление с отдельными случаями для ваш объект, строка и целые значения:

struct ConfigData {
    let configData: [ConfigDatumElement]
}

enum ConfigDatumElement {
    case configDatumClass(ConfigDatumClass)
    case integer(Int)
    case string(String)
}

struct ConfigDatumClass {
    let name, configTitle: String
}

Вот полный пример кода. Немного сложно расшифровать enum, но quicktype поможет вам:

// To parse the JSON, add this file to your project and do:
//
//   let configData = try? JSONDecoder().decode(ConfigData.self, from: jsonData)

import Foundation

struct ConfigData: Codable {
    let configData: [ConfigDatumElement]

    enum CodingKeys: String, CodingKey {
        case configData = "config_data"
    }
}

enum ConfigDatumElement: Codable {
    case configDatumClass(ConfigDatumClass)
    case integer(Int)
    case string(String)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Int.self) {
            self = .integer(x)
            return
        }
        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }
        if let x = try? container.decode(ConfigDatumClass.self) {
            self = .configDatumClass(x)
            return
        }
        throw DecodingError.typeMismatch(ConfigDatumElement.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ConfigDatumElement"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .configDatumClass(let x):
            try container.encode(x)
        case .integer(let x):
            try container.encode(x)
        case .string(let x):
            try container.encode(x)
        }
    }
}

struct ConfigDatumClass: Codable {
    let name, configTitle: String

    enum CodingKeys: String, CodingKey {
        case name
        case configTitle = "config_title"
    }
}

Хорошо использовать enum, потому что таким образом вы получаете максимальную безопасность типов. Другие ответы, кажется, теряют это.

Используя опцию удобных инициализаторов quicktype, пример рабочего кода:

let data = try ConfigData("""
{
  "config_data": [
    {
      "name": "illuminate",
      "config_title": "Blink"
    },
    {
      "name": "shoot",
      "config_title": "Fire"
    },
    "illuminate",
    "shoot",
    25,
    100
  ]
}
""")

for item in data.configData {
    switch item {
    case .configDatumClass(let d):
        print("It's a class:", d)
    case .integer(let i):
        print("It's an int:", i)
    case .string(let s):
        print("It's a string:", s)
    }
}

Это печатает:

It's a class: ConfigDatumClass(name: "illuminate", configTitle: "Blink")
It's a class: ConfigDatumClass(name: "shoot", configTitle: "Fire")
It's a string: illuminate
It's a string: shoot
It's an int: 25
It's an int: 100
person David Siegel    schedule 28.02.2018
comment
Я просто прохожу через это и пытаюсь понять, как мы можем получить доступ к значению из configData: [ConfigDatumElement] - person Ganesh Somani; 28.02.2018
comment
Я добавил пример рабочего кода — вы можете выполнить итерацию по этому массиву и использовать оператор switch, как я продемонстрировал. - person David Siegel; 28.02.2018

Сначала вам нужно решить, что делать, если появится второй JSON. Второй формат JSON содержит меньше информации. Что вы хотите сделать с этими данными (config_title), которые вы потеряли? Они вообще вам нужны?

Если вам нужно сохранить config_title, если они присутствуют, я предлагаю вам создать структуру ConfigItem, которая выглядит следующим образом:

struct ConfigItem: Codable {
    let name: String
    let configTitle: String?

    init(name: String, configTitle: String? = nil) {
        self.name = name
        self.configTitle = configTitle
    }

    // encode and init(decoder:) here...
    // ...
}

Реализуйте необходимые методы encode и init(decoder:). Вы знаете, что делать.

Теперь, когда вы декодируете свой JSON, декодируйте ключ config_data как обычно. Но на этот раз вместо [Any] можно декодировать до [ConfigItem]! Очевидно, что это не всегда будет работать, потому что иногда JSON может быть во второй форме. Таким образом, вы ловите любую ошибку, возникающую из этого, и декодируете config_data, используя вместо этого [String]. Затем сопоставьте массив строк с набором ConfigItems!

person Sweeper    schedule 18.01.2018
comment
Обновил мой вопрос. Основная вещь заключается в том, что массив может содержать любые данные - person Ganesh Somani; 18.01.2018
comment
@GaneshSomani Как я уже сказал в начале, что вы хотите делать с JSON, если это может быть что угодно? Что вы пытаетесь в конечном итоге сделать? - person Sweeper; 18.01.2018
comment
Это некоторая избыточная информация, которая поддерживает мои основные данные... Например, я храню счетчик, поэтому он может быть начальным начальным счетчиком, который я могу получить с сервера. Или это дополнительная информация для музыкальных данных, тогда она может содержать URL обложек альбомов. - person Ganesh Somani; 18.01.2018
comment
@GaneshSomani Итак, вы знаете, что получаете. Вы можете сказать по основным данным, верно? Затем создайте структуру для каждой из этих ситуаций. И декодируйте, используя правильный тип структуры. Или вам придется преобразовать JSON в словарь, используя способ Swift 3. - person Sweeper; 18.01.2018

Вы пытаетесь JSON to object или object to JSON ? вы можете попробовать этот код добавить любой быстрый файл:

extension String {
    var xl_json: Any? {
        if let data = data(using: String.Encoding.utf8) {
            return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
        }
        return nil
    }
}

extension Array {
    var xl_json: String? {
        guard let data = try? JSONSerialization.data(withJSONObject: self, options: []) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }
}

extension Dictionary {
    var xl_json: String? {
        guard let data = try? JSONSerialization.data(withJSONObject: self, options: []) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }
}

и запустите этот код:

let str = "{\"key\": \"Value\"}"
let dict = str.xl_json as! [String: String] // JSON to Objc
let json = dict.xl_json                     // Objc to JSON

print("jsonStr - \(str)")
print("objc - \(dict)")
print("jsonStr - \(json ?? "nil")")

Наконец, вы получите это:

jsonStr - {"key": "Value"}
objc - ["key": "Value"]
jsonStr - {"key":"Value"}
person xx11dragon    schedule 18.01.2018
comment
JSONSerialization всегда было проще выполнить, но с Swift 4 и JSONDecoder у меня возникают проблемы с этим. - person Ganesh Somani; 18.01.2018