问题描述
我有以下从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,其中数据和“列标头"在单独的数组中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!