2024-10-23 18:39:46
,某些文章具有时效性,若有错误或已失效,请在下方留言。在本文中,将向您展示如何以优雅的方式处理任何类型的 JSON,而无需依赖第三方库。
Codable 的不足之处
Swift 的 Codable
实现与魔法相去不远:如果您有完美的 JSON
,那么您几乎不需要做任何工作就可以转换成您自己的数据类型,即使您有不完美的 JSON
,您也可以添加一个自定义初始化器,并且仍然可以获得几乎立竿见影的效果。
即使是漂亮的 JSON,有时您也只需要所给数据的一小部分–也许是嵌套在输入内容深处的一个非常特殊的值。在这里使用 Codable
意味着要自己创建大量结构体,而这些结构体最终都不会被使用。
为了演示有问题的输入,这里有一个完全非结构化 JSON 的示例:
let json = """
[
{
"name": "Taylor Swift",
"company": "Taytay Inc",
"age": 26,
"address": {
"street": "555 Taylor Swift Avenue",
"city": "Nashville",
"state": "Tennessee",
"gps": {
"lat": 36.1868667,
"lon": -87.0661223
}
}
},
{
"title": "1989",
"type": "studio",
"year": "2014",
"singles": 7
},
{
"title": "Shake it Off",
"awards": 10,
"hasVideo": true
}
]
"""
这是一个数组,包括一个人、一张专辑和专辑中的一首歌,所有这些都混在一起。如果你愿意,完全可以尝试用 Codable
来解码,但这样做并不愉快–尤其是如果你只想从中获取一小段数据的话。
手动挖掘
您最初的解决方案可能是手工挖掘 JSON,这可能是最有效的方法。您可以先将 JSON 转换为数据实例,如下所示:
let data = Data(json.utf8)
接下来,把 JSON 解析成一个 [String: Any]
的字典数组:
if let objects = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
// more code to come
}
现在,你可以循环读取数组中的每个项,并开始读取数值。例如,如果您想打印出标题键(如果存在的话),您可以将 // more code to come 替换为下面的注释:
for object in objects {
if let title = object["title"] as? String {
print(title)
}
}
这样做是可行的,但很快就会变得很烦人–我们需要对所有内容进行类型转换。如果你想让事情变得简单一些,你可以把这些类型转换封装成字典的扩展方法,允许你为给定的键请求特定的类型:
extension Dictionary where Key == String {
func bool(for key: String) -> Bool? {
self[key] as? Bool
}
func string(for key: String) -> String? {
self[key] as? String
}
func int(for key: String) -> Int? {
self[key] as? Int
}
func double(for key: String) -> Double? {
self[key] as? Double
}
}
现在,if let
代码变得简单多了:
if let title = object.string(for: "title") {
print(title)
}
这很有效,但我们可以做得更好!
构建 JSON 类型
为了更轻松地处理非结构化数据,我们可以构建一个完全自定义的 JSON 类型,将 JSONSerialization 封装在大量辅助属性和方法中。这些属性和方法的编写非常简单,但却能让我们以自然、近乎美观的方式解析任何类型的 JSON。
在其核心部分,这个新的 JSON 结构将有两种类型的访问器:一种是返回某种查询(”把数组中的第三个项目作为字典给我,然后从字典中读取键’name'”),另一种是返回某种值(”我希望这个东西是一个字符串”)。
在类型层面,我们不知道我们正在处理的数据是什么类型:它可能是一个名字数组、一个名字、一个名字和分数的字典、一个名字和分数的字典数组–简直无所不能。也可能什么都不是:如果我们实际上有一个名称数组,而我们要求我们的数据是名称字典,那么我们就没有匹配的数据。
那么,我们要存储的数据是 Any?
– 任何数据,或者什么数据都没有。让我们从这里开始:
struct JSON: RandomAccessCollection {
var value: Any?
}
我们将为结构提供两个初始化器:一个用于根据 JSON 字符串创建 JSON 实例,另一个用于接受另一个 Any?,这样我们就可以随时创建它们:
init(string: String) throws {
let data = Data(string.utf8)
value = try JSONSerialization.jsonObject(with: data)
}
init(value: Any?) {
self.value = value
}
现在是我提到的两类访问器:查询和值。值的部分是最简单的,因为它们会将我们的值转换为所请求的任何类型的可选和非可选值。
首先,将这些属性添加到 JSON 结构中:
var optionalBool: Bool? {
value as? Bool
}
var optionalDouble: Double? {
value as? Double
}
var optionalInt: Int? {
value as? Int
}
var optionalString: String? {
value as? String
}
我还说过,我们需要非可选值,但我们不希望这些值有危险–我们不希望它们崩溃。因此,如果可选项为空,这四个属性的非可选项等价物将返回默认值。在实践中,这意味着处理 JSON 的人可以说 “给我一个可选字符串”(如果他们想手动检查的话),或者说 “给我一个真正的字符串(总是)”(如果他们不介意在键丢失的情况下得到一个默认值的话)。
var bool: Bool {
optionalBool ?? false
}
var double: Double {
optionalDouble ?? 0
}
var int: Int {
optionalInt ?? 0
}
var string: String {
optionalString ?? ""
}
事情变得棘手的地方在于我们如何处理数组
和字典
。我们知道当前值是 Any?
,但如果要将其类型转换为数组,我们需要知道数组中包含了什么–Swift 的数组需要在某种类型上进行泛型。
当他们要求我们将数据转换成数组
时,我们首先将我们的值转换成 Any 数组,然后再将其映射,这样我们的数组值实际上就是使用 Any 值的 JSON 类型的新实例。这样,我们就可以自由地查询数据,并创建一个非常简洁的 API。
这其实很容易做到,只需添加此属性即可:
var optionalArray: [JSON]? {
let converted = value as? [Any]
return converted?.map { JSON(value: $0) }
}
对于字典,我们需要将我们的对象转换为 [String: Any]
字典,因为我们知道键总是字符串,但我们可以像处理数组一样映射值:
var optionalDictionary: [String: JSON]? {
let converted = value as? [String: Any]
return converted?.mapValues { JSON(value: $0) }
}
我们还可以添加非选项等价物,根据需要发送默认值:
var array: [JSON] {
optionalArray ?? []
}
var dictionary: [String: JSON] {
optionalDictionary ?? [:]
}
这样就完成了所有属性值的读取,现在我们只需添加查询部分。这将使我们能够干净利落地挖掘 JSON:我们要编写自定义下标,这样就可以直接将对象视为数组或字典,而不是总是使用数组和字典访问器。
在 Swift 中,这些只是简单的下标,事实上我们可以让它们直接调用我们刚刚创建的属性。如果任何一个访问器都没有请求的值,我们就会返回一个空值:
subscript(index: Int) -> JSON {
optionalArray?[index] ?? JSON(value: nil)
}
subscript(key: String) -> JSON {
optionalDictionary?[key] ?? JSON(value: nil)
}
为了使代码能够编译,我们需要做两个小的补充:为了符合 RandomAccessCollection
协议,我们必须提供 startIndex
和 endIndex
属性,但实际上我们可以再次将这些属性传递给我们已有的代码。在这种情况下,我们可以使用数组属性,它可以为我们完成工作–现在就添加这两个属性:
var startIndex: Int { array.startIndex }
var endIndex: Int { array.endIndex }
这就是我们的 JSON 类型,现在我想向大家展示它在 JSON 解析方面的神奇之处。
例如,如果我们想加载我们的 JSON 字符串并打印我们找到的所有标题,我们可以这样写
let object = try JSON(string: json)
for item in object {
print(item["title"].string)
}
使用该字符串访问器意味着第一个对象的字符串为空(因为它的字符串是 “name “而不是 “title”),但后面两个对象的字符串都是空的。
因为我们已经让字典访问返回另一个 JSON 对象,所以我们可以进一步挖掘:
print(item["address"]["city"].string)
我们能挖多远就挖多远:
if let latitude = item["address"]["gps"]["lat"].optionalDouble {
print("Latitude is \(latitude)")
}
如果其中任何一项失效,整行将自动失效–它不会让我们的代码崩溃,而只是像普通可选项一样短路。
我希望你会同意这是一个巨大的进步,但我们还可以做得更好…
添加动态成员
我们将为 JSON 结构添加一个很少用到的功能,它能让我们更轻松地处理值。它被称为 @dynamicMemberLookup
,它允许 Swift 在运行时动态访问属性,而不必全部写入。
首先,在结构体中添加属性:
@dynamicMemberLookup
struct JSON: RandomAccessCollection {
现在添加这个新的下标,它的作用与我们的普通字典下标完全相同:
subscript(dynamicMember key: String) -> JSON {
optionalDictionary?[key] ?? JSON(value: nil)
}
就是这样。我知道看起来我们几乎什么都没改,但看看这对我们的最终代码有什么影响:
if let latitude = item.address.gps.lat.optionalDouble {
print("Latitude is \(latitude)")
}
从字面上看,它将字典访问转换成了常规的属性访问,同时类型也同样安全–在这段代码中,我们仍然会得到一个可选的 Double。
暂无评论内容