解析数据和"column headers"位于不同数组中的复杂 JSON



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

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

您仍然可以使用 JSONDecoder,但您需要手动解码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)
}
}

要使用它,您可以使用正常的 JSONDecode 方式:

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
}

看起来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..<$0.count {
columnValues[columns[i], default: []].append($0[i])
}
}
}
}
struct Column: Codable, Hashable {
let name: String
let type: String
}

下一步是为数据引入结构

我这样做的方法是创建两个模型对象并让它们都符合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
...

在这种情况下,dataURLSessionTask的结果。

让我知道这是否有效。

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

这是我编写的示例,您可以向列表中添加新的用户对象,而不是记录信息,其中包含当前打印的信息。

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 中这样做了,因此可能需要进行一些调整才能使代码在生产中工作。虽然我认为这个想法是显而易见的。

附言我的实现不处理"数据表"。应该很容易添加,但我认为这只会增加我的答案长度而不会提供任何好处。毕竟,挑战在于该领域:)

最新更新