解析复杂的 JSON,其中数据和 "column headers" 在不同的数组中

Parsing complex JSON where data and "column headers" are in separate arrays

我从 API 获得以下 JSON 数据:

{"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" }
  ]}
}

稍后的查询可能会产生以下结果:

{"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" }
  ]
  }
}

列的顺序似乎是流动的。

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

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] { ... }。 更不用说随后循环遍历列数组来组织数据了。

有更好的解决办法吗? 谢谢

我的方法是创建两个模型对象并让它们都符合 Codable 协议,如下所示:

struct Datatable: Codable {
    let data: [[String]]
    let columns: [[String: String]]
}

struct JSONResponseType: Codable {
    let datatable: Datatable
}

然后在您的网络调用中,我将使用 JSONDecoder():

解码 json 响应
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let decodedData = try? decoder.decode(JSONResponseType.self, from: data) else {
    // handle decoding failure
    return
}

// do stuff with decodedData ex:

let datatable = decodedData.datatable
...

data 在这种情况下是 URLSessionTask.

的结果

让我知道这是否有效。

也许尝试将给定的输入保存在用户对象列表中?然而,这种方式 JSON 是结构化的,您可以将它们添加到列表中并在您喜欢之后处理它们。也许在姓名后的初始字母顺序也有助于用户的显示顺序。

这是我写的示例,您可以将新的 UserObject 添加到包含当前打印信息的列表中,而不是记录信息。

let databaseData =  table["datatable"]["data"];
let databaseColumns = table["datatable"]["columns"];

for (let key in databaseData) { 
    console.log(databaseColumns[0]["name"] + " = " + databaseData[key][0]);
    console.log(databaseColumns[1]["name"] + " = " + databaseData[key][1]);
    console.log(databaseColumns[2]["name"] + " = " + databaseData[key][2]);    
    console.log(databaseColumns[3]["name"] + " = " + databaseData[key][3]);
}

我唯一能想到的是:

struct ComplexValue {
    var value:String
    var columnName:String
    var type:String
}

struct ComplexJSON: Decodable, Encodable {
    enum CodingKeys: String, CodingKey {
        case data, columns
    }

    var data:[[String]]
    var columns:[ColumnSpec]
    var processed:[[ComplexValue]]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        data = (try? container.decode([[String]].self, forKey: .data)) ?? []
        columns = (try? container.decode([ColumnSpec].self, forKey: .columns)) ?? []
        processed = []
        for row in data {
            var values = [ComplexValue]()
            var i = 0
            while i < columns.count {
                var item = ComplexValue(value: row[i], columnName: columns[i].name, type: columns[i].type)
                values.append(item)
                i += 1
            }
            processed.append(values)
        }
    }
}

struct ColumnSpec: Decodable, Encodable {
    enum CodingKeys: String, CodingKey {
        case name, type
    }

    var name:String
    var type:String

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = (try? container.decode(String.self, forKey: .name)) ?? ""
        type = (try? container.decode(String.self, forKey: .type)) ?? ""
    }
}

现在您将拥有 processed 变量,其中包含数据的格式化版本。好吧,格式化可能不是最好的词,因为结构是完全动态的,但至少当你提取一些特定的单元格时,你会知道它的值、类型和它的列名。

如果没有关于您的 API 的额外详细信息,我认为您不能做比这更具体的事情。

另外,请注意我是在 Playground 中完成的,因此可能需要进行一些调整才能使代码在生产环境中运行。虽然我觉得思路清晰可见。

P.S。我的实现不处理 "datatable"。添加应该很简单,但我认为这只会增加我的答案的长度而不会提供任何好处。毕竟,挑战在那个领域内:)

datacolumns 值似乎以相同的顺序编码,因此我们可以使用它为值的列和数组创建字典,其中每个数组的顺序相同。

struct Root: Codable {
    let datatable: Datatable
}

struct Datatable: Codable {
    let data: [[String]]
    let columns: [Column]
    var columnValues: [Column: [String]]

    enum CodingKeys: String, CodingKey {
        case data, columns
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        data = try container.decode([[String]].self, forKey: .data)
        columns = try container.decode([Column].self, forKey: .columns)

        columnValues = [:]
        data.forEach {
            for i in 0..<[=10=].count {
                columnValues[columns[i], default: []].append([=10=][i])
            }
        }
    }
}

struct Column: Codable, Hashable {
    let name: String
    let type: String
}

下一步是为数据引入结构

您仍然可以使用 JSON解码器,但您需要手动解码 data 数组。

为此,您需要读取列数组,然后使用从列数组中获得的顺序解码数据数组。

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

设置如下:

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)
   }
}

要使用它,您需要使用正常的JSON解码方式:

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 支持的基础原始类型(数字、字符串、布尔值或空值)。它看起来像这样:

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
  }
}

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

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

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

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
  }