在Swift中解析JSON的正确方法

解析JSON的正确方法是使用Codable。 但是Codable的问题在于,如果您有嵌套的JSON,则必须编写额外的Codable结构或编写自定义初始化程序。 这就是为什么许多开发人员选择使用诸如SwiftyJSON之类的第三方解决方案的原因。

但这就是失败的道路。 让我们对其进行修复,并结合SwiftyJSON和Codable的最佳部分来制作下一个JSON解析库。

这个主意

有两种从JSON解析嵌套结构的方法:创建额外的Codable结构或创建自定义初始化程序。 让我们考虑一下如何实现两者的自动化。

第一种方法可以通过代码生成实现自动化。 但是要获得它,要花很多时间才能使它起作用,并且您必须具有一些JSON的形式化描述。

让我们仔细看看创建自定义初始化程序的第二种方法。 简要地说,我们必须描述每个嵌套级别的编码键。 然后,我们可以使用这些编码键来创建嵌套容器。 看一下示例JSON:

 让json = [ 
“容器”: [
“对象”:[
“名称”:“阿纳金·天行者”,
“ alias”:“ Darth Vader”,
“职业”:“西斯黑暗之王”,
“年龄”:42岁
]
]
]
让jsonData =尝试JSONSerialization.data(withJSONObject:json,选项:[])

为了解析它,我们必须创建一个包含额外编码键的结构:

  struct Human:可编码{ 
命名:字符串
让别名:字符串
让职业:字符串
年龄:整数
 枚举ContainerKeys:字符串,CodingKey { 
箱子容器
}
 枚举ObjectKeys:字符串,CodingKey { 
案例对象
}
  public init(来自解码器:Decoder)抛出{ 
让容器=尝试解码器。容器(
keyedBy:ContainerKeys.self

让嵌套=尝试container.nestedContainer(
keyedBy:ObjectKeys.self,
forKey:.container

让subnestedContainer =尝试nested.nestedContainer(
keyedBy:CodingKeys.self,
forKey:.object
 名称=尝试subnested.decode(String.self,forKey:.name) 
别名=试试subnested.decode(String.self,forKey:.alias)
职业=尝试subnested.decode(String.self,
forKey:.occupation)
年龄=尝试subnested.decode(Int.self,forKey:.age)
}
}

解析这种简单结构的大量额外代码。 副作用是我们损失了一半的可编码代码。 我们正在手动解析所有属性。 但是,这里的常规工作是创建一个编码键枚举并为其获取一个嵌套容器。 实际上,CodingKey实现不一定是枚举。 它只需要具有一个字符串值和一个int值。 以及两者的初始化程序。 正确的想法是,我们可以使用变量键创建CodingKey的实现。

使编码密钥可重用

  struct Key:CodingKey { 
let stringValue:字符串
let intValue:整数?
 初始化?(stringValue:String){ 
self.stringValue =字符串值
self.intValue = Int(字符串值)
}
 初始化?(intValue:Int){ 
self.intValue = intValue
self.stringValue =字符串(描述:intValue)
}
}

现在,我们可以重写Human结构:

  struct Human:可编码{ 
命名:字符串
让别名:字符串
让职业:字符串
年龄:整数
  public init(来自解码器:Decoder)抛出{ 
让containerKey = Key(stringValue:“ container”)!
让objectKey =键(stringValue:“对象”)!
 让容器=尝试解码器。容器(keyedBy:Key.self) 
让嵌套=尝试container.nestedContainer(
keyedBy:Key.self,
forKey:容器密钥

让subnested =尝试nested.nestedContainer(
keyedBy:CodingKeys.self,
forKey:objectKey
 名称=尝试subnested.decode(String.self,forKey:.name) 
别名=试试subnested.decode(String.self,forKey:.alias)
职业=尝试subnested.decode(String.self,
forKey:.occupation)
年龄=尝试subnested.decode(Int.self,forKey:.age)
}
}

JSON解码路径

这种做法为我们节省了几行代码,但是我们仍然必须编写自定义初始化程序,并且损失了一半的Codable代码生成。 让我们创建一个通用的JSON提取器,以删除从模型初始化程序创建嵌套容器的代码。

 结构JSONPath:可解码{ 
让根:KeyedDecodingContainer
  init(来自解码器:解码器)抛出{ 
// 1
self.root =尝试解码器。容器(keyedBy:Key.self)
}
  func value (atPath:String ...)抛出-> T { 
// 2
var keys = atPath.compactMap(Key.init(stringValue :))
让valueKey = keys.popLast()!
// 3
让容器=试试keys.reduce(root){
尝试$ 0.nestedContainer(keyedBy:Key.self,forKey:$ 1)
}
// 4
返回try container.decode(T.self,forKey:valueKey)
}
}
  1. 在来自解码器的初始化中,我们保存以CodingKey的变量实现为键的JSON的根容器。 我们以后不需要保存整个解码器,这种方法以后可以为我们节省1行样板。
  2. 在这里,我们将路径从[String]类型转换为[Key]并将其分为两部分。 最后一个键是可以找到目标值的键。 路径的最后一部分仅包含嵌套容器。
  3. 我们为所有给定键创建嵌套容器,直到最后,然后保存最深的容器
  4. 我们采用最深的容器和最后一个键,并从此处解码目标可Decodable

因此,现在我们可以将“人工”实现简化为:

  struct Human:可编码{ 
命名:字符串
让别名:字符串
让职业:字符串
年龄:整数
}

而已。 现在我们可以用几行代码来解析人类

 让json =试试JSONDecoder()。decode(JSONPath.self,来自:jsonData) 
let human:Human = try json.value(atPath:“ container”,“ object”)

从现在到哪里

看看最后的游乐场。 有一个基于本文描述的思想的工作库。 它不仅支持字符串键,还支持数组索引。 在这里您可以使用dynamicMemberLookup描述路径。 敬请期待本文的下一部分。 在那里,我将整理如何将数组索引添加到路径并将它们与字符串键组合在一起。