解析复杂的 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"。添加应该很简单,但我认为这只会增加我的答案的长度而不会提供任何好处。毕竟,挑战在那个领域内:)
data
和 columns
值似乎以相同的顺序编码,因此我们可以使用它为值的列和数组创建字典,其中每个数组的顺序相同。
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
}
我从 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()
:
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"。添加应该很简单,但我认为这只会增加我的答案的长度而不会提供任何好处。毕竟,挑战在那个领域内:)
data
和 columns
值似乎以相同的顺序编码,因此我们可以使用它为值的列和数组创建字典,其中每个数组的顺序相同。
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
}