本文介绍了解析复杂的JSON,其中数据和“列标头"在单独的数组中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有以下从API获取的JSON数据:

I have the following JSON data I get from an API:

{"datatable":
  {"data" : [
    ["John", "Doe", "1990-01-01", "Chicago"],
    ["Jane", "Doe", "2000-01-01", "San Diego"]
  ],
  "columns": [
    { "name": "First", "type": "String" },
    { "name": "Last", "type": "String" },
    { "name": "Birthday", "type": "Date" },
    { "name": "City", "type": "String" }
  ]}
}

以后的查询可能会导致以下结果:

A later query could result the following:

{"datatable":
  {"data" : [
    ["Chicago", "Doe", "John", "1990-01-01"],
    ["San Diego", "Doe", "Jane", "2000-01-01"]
  ],
  "columns": [
    { "name": "City", "type": "String" },
    { "name": "Last", "type": "String" },
    { "name": "First", "type": "String" },
    { "name": "Birthday", "type": "Date" }
  ]
  }
}

列的顺序似乎很不稳定.

The order of the colums seems to be fluid.

我最初想用JSONDecoder解码JSON,但是为此,我需要将数据数组作为字典而不是数组.我唯一想到的另一种方法是将结果转换为字典,例如:

I initially wanted to decode the JSON with JSONDecoder, but for that I need the data array to be a dictionary and not an array.The only other method I could think of was to convert the result to a dictionary with something like:

extension String {
    func convertToDictionary() -> [String: Any]? {
        if let data = data(using: .utf8) {
            return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
        }
        return nil
    }
}

这将导致我有很多嵌套的if let语句,例如if let x = dictOfStr["datatable"] as? [String: Any] { ... }.更不用说随后循环遍历columns数组来组织数据了.

This will cause me however to have a lot of nested if let statements like if let x = dictOfStr["datatable"] as? [String: Any] { ... }.Not to mention the subsequent looping through the columns array to organize the data.

有更好的解决方案吗?谢谢

Is there a better solution?Thanks

推荐答案

您仍然可以使用JSONDecoder,但是您需要手动解码data数组.

You could still use JSONDecoder, but you'd need to manually decode the data array.

要做到这一点,您需要读取columns数组,然后使用从columns数组中获得的顺序对数据数组进行解码.

To do that, you'd need to read the columns array, and then decode the data array using the ordering that you got from the columns array.

这实际上是KeyPath的一个很好的用例.您可以创建列到对象属性的映射,这有助于避免使用较大的switch语句.

This is actually a nice use case for KeyPaths. You can create a mapping of columns to object properties, and this helps avoid a large switch statement.

所以这是设置:

struct DataRow {
  var first, last, city: String?
  var birthday: Date?
}

struct DataTable: Decodable {

  var data: [DataRow] = []

  // coding key for root level
  private enum RootKeys: CodingKey { case datatable }

  // coding key for columns and data
  private enum CodingKeys: CodingKey { case data, columns }

  // mapping of json fields to properties
  private let fields: [String: PartialKeyPath<DataRow>] = [
     "First":    \DataRow.first,
     "Last":     \DataRow.last,
     "City":     \DataRow.city,
     "Birthday": \DataRow.birthday ]

  // I'm actually ignoring here the type property in JSON
  private struct Column: Decodable { let name: String }

  // init ...
}

现在init功能:

init(from decoder: Decoder) throws {
   let root = try decoder.container(keyedBy: RootKeys.self)
   let inner = try root.nestedContainer(keyedBy: CodingKeys.self, forKey: .datatable)

   let columns = try inner.decode([Column].self, forKey: .columns)

   // for data, there's more work to do
   var data = try inner.nestedUnkeyedContainer(forKey: .data)

   // for each data row
   while !data.isAtEnd {
      let values = try data.decode([String].self)

      var dataRow = DataRow()

      // decode each property
      for idx in 0..<values.count {
         let keyPath = fields[columns[idx].name]
         let value = values[idx]

         // now need to decode a string value into the correct type
         switch keyPath {
         case let kp as WritableKeyPath<DataRow, String?>:
            dataRow[keyPath: kp] = value
         case let kp as WritableKeyPath<DataRow, Date?>:
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "YYYY-MM-DD"
            dataRow[keyPath: kp] = dateFormatter.date(from: value)
         default: break
         }
      }

      self.data.append(dataRow)
   }
}

要使用此功能,请使用常规的JSONDecode方法:

To use this, you'd use the normal JSONDecode way:

let jsonDecoder = JSONDecoder()
let dataTable = try jsonDecoder.decode(DataTable.self, from: jsonData)

print(dataTable.data[0].first) // prints John
print(dataTable.data[0].birthday) // prints 1990-01-01 05:00:00 +0000

编辑

上面的代码假定JSON数组中的所有值都是字符串,并尝试执行decode([String].self).如果您不能做出这样的假设,则可以将值解码为JSON支持的基础基本类型(数字,字符串,布尔值或null).看起来像这样:

The code above assumes that all the values in a JSON array are strings and tries to do decode([String].self). If you can't make that assumption, you could decode the values to their underlying primitive types supported by JSON (number, string, bool, or null). It would look something like this:

enum JSONVal: Decodable {
  case string(String), number(Double), bool(Bool), null, unknown

  init(from decoder: Decoder) throws {
     let container = try decoder.singleValueContainer()

     if let v = try? container.decode(String.self) {
       self = .string(v)
     } else if let v = try? container.decode(Double.self) {
       self = .number(v)
     } else if ...
       // and so on, for null and bool
  }
}

然后,在上面的代码中,将数组解码为以下值:

Then, in the code above, decode the array into these values:

let values = try data.decode([JSONValue].self)

稍后,当您需要使用该值时,可以检查基础值并决定要做什么:

Later when you need to use the value, you can examine the underlying value and decide what to do:

case let kp as WritableKeyPath<DataRow, Int?>:
  switch value {
    case number(let v):
       // e.g. round the number and cast to Int
       dataRow[keyPath: kp] = Int(v.rounded())
    case string(let v):
       // e.g. attempt to convert string to Int
       dataRow[keyPath: kp] = Int((Double(str) ?? 0.0).rounded())
    default: break
  }

这篇关于解析复杂的JSON,其中数据和“列标头"在单独的数组中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-05 23:32