使用可解码的动态键解码和扁平化JSON
hudson 译 原文
可解码协议Decodable是在Swift 4中引入的。 从那时起,它已成为开发人员解码从远程服务器接收的JSON的标准方式。
有大量的教程教你如何利用Decodable来解码各种类型的JSON结构。 然而,所有这些教程都没有涵盖一种特定类型的JSON结构——带有动态键的JSON。
带有动态键的JSON是什么意思? 看看以下显示学生列表的JSON示例:
{
“S001”: {
“firstName”: “Tony”,
“lastName”: “Stark”
},
“S002”: {
“firstName”: “Peter”,
“lastName”: “Parker”
},
“S003”: {
“firstName”: “Bruce”,
“lastName”: “Wayne”
}
}
如您所见,学生ID是键,学生信息是值。
作为iOS开发人员,我们通常需要的是一组学生,以便学生名单可以轻松地显示在表格视图上。 因此,除了解码JSON外,我们还需要将结果扁平化(使学生ID成为学生对象的一部分)并将其转换为数组。
当面对这种JSON结构时,一些开发人员可能会倒退回旧的解码方法:通过使用JSONSerialization类并手动循环和解析每个键值对 。
然而,我不喜欢JSONSerialization方式,因为它更容易出错。 除此之外,我们将失去使用Decodable协议带来的所有好处。
在本文中,我将向您介绍使用Decodable可解码协议的解码方法。 之后,我将使解码逻辑通用,以便它可以被其他对象类型重用。
说了这么多,让我们直接进入主题。
提取值
回顾一下,这是我们试图解码的JSON:
{
“S001”: {
“firstName”: “Tony”,
“lastName”: “Stark”
},
“S002”: {
“firstName”: “Peter”,
“lastName”: “Parker”
},
“S003”: {
“firstName”: “Bruce”,
“lastName”: “Wayne”
}
}
为了简单起见,让我们暂时专注于解码firstName名字和lastName。我们稍后会回到学生ID上。
首先,让我们定义一个符合Decodable协议的Student结构。
struct Student: Decodable {
let firstName: String
let lastName: String
}
接下来,我们需要一个包含学生数组的Decodable结构。我们将使用此结构来保存所有已解码的学生对象。让我们把这个结构称为DecodedArray。
struct DecodedArray: Decodable {
var array: [Student]
}
为了访问JSON的动态键,我们必须定义自定义CodingKey结构。当我们想从JSONDecoder创建解码容器时,需要这个自定义CodingKey结构。
struct DecodedArray: Decodable {
var array: [Student]
// Define DynamicCodingKeys type needed for creating
// decoding container from JSONDecoder
private struct DynamicCodingKeys: CodingKey {
// Use for string-keyed dictionary
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
}
请注意,我们只对字符串值初始化器感兴趣,因为我们的键是字符串类型,因此我们可以在整数值初始化器中返回nil。
有了所有这些,我们现在可以开始实现DecodedArray初始化器了。
struct DecodedArray: Decodable {
var array: [Student]
// Define DynamicCodingKeys type needed for creating
// decoding container from JSONDecoder
private struct DynamicCodingKeys: CodingKey {
// Use for string-keyed dictionary
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
init(from decoder: Decoder) throws {
// 1
// Create a decoding container using DynamicCodingKeys
// The container will contain all the JSON first level key
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var tempArray = [Student]()
// 2
// Loop through each key (student ID) in container
for key in container.allKeys {
// Decode Student using key & keep decoded Student object in tempArray
let decodedObject = try container.decode(Student.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
// 3
// Finish decoding all Student objects. Thus assign tempArray to array.
array = tempArray
}
}
让我们详细分解一下初始化器内部发生的事情。
-
使用
DynamicCodingKeys结构创建一个解码容器。此容器将包含所有JSON的第一级动态键。 -
循环每个键来解码其各自的学生对象。
-
将所有解码的学生对象存储到学生数组中。
提取firstName和lastName就是这样。让我们在 Xcode playground中运行所有这些,看看它们的运行情况。
let jsonString = “””
{
“S001”: {
“firstName”: “Tony”,
“lastName”: “Stark”
},
“S002”: {
“firstName”: “Peter”,
“lastName”: “Parker”
},
“S003”: {
“firstName”: “Bruce”,
“lastName”: “Wayne”
}
}
“””
let jsonData = Data(jsonString.utf8)
// Ask JSONDecoder to decode the JSON data as DecodedArray
let decodedResult = try! JSONDecoder().decode(DecodedArray.self, from: jsonData)
dump(decodedResult.array)
// Output:
//▿ 3 elements
//▿ __lldb_expr_21.Student
// - firstName: “Bruce”
// - lastName: “Wayne”
//▿ __lldb_expr_21.Student
// - firstName: “Peter”
// - lastName: “Parker”
//▿ __lldb_expr_21.Student
// - firstName: “Tony”
// - lastName: “Stark”
在这里,我们将JSON字符串转换为Data,并要求JSONDecoder将JSON数据解码为DecodedArray类型。据此我们将能够通过DecodedArray.array访问所有解码的学生对象。
恭喜!您已成功解码所有学生对象。然而,还有工作要做。在下一节中,我们将研究将学生ID添加到学生结构中。
提取键
以我们目前的基础,将学生ID添加到学生结构中非常简单。我们需要做的是实现我们自己的Student初始化器,并手动解码lastName、firstName和studentId。
看看以下更新的学生结构:
struct Student: Decodable {
let firstName: String
let lastName: String
// 1
// Define student ID
let studentId: String
// 2
// Define coding key for decoding use
enum CodingKeys: CodingKey {
case firstName
case lastName
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// 3
// Decode firstName & lastName
firstName = try container.decode(String.self, forKey: CodingKeys.firstName)
lastName = try container.decode(String.self, forKey: CodingKeys.lastName)
// 4
// Extract studentId from coding path
studentId = container.codingPath.first!.stringValue
}
}
让我们逐一介绍我们在Student结构上所做的更改:
-
定义
studentId以保存提取键(学生ID)。 -
定义手动解码所需的编码键。
-
手动解码
firstName和lastName。 -
这就是魔法发生的地方。解码容器
codingPath是CodingKey的数组,其中包含在解码中达到这一点的编码键路径。就我们而言,它应该包含我们从DecodedArray中的DynamicCodingKeys获得键,即学生ID。
让我们在Xcode playground中再次运行它,看看最终结果。
let jsonData = Data(jsonString.utf8)
let decodedResult = try! JSONDecoder().decode(DecodedArray.self, from: jsonData)
dump(decodedResult.array)
// Output:
//▿ 3 elements
//▿ __lldb_expr_37.Student
// - firstName: “Peter”
// - lastName: “Parker”
// - studentId: “S002”
//▿ __lldb_expr_37.Student
// - firstName: “Tony”
// - lastName: “Stark”
// - studentId: “S001”
//▿ __lldb_expr_37.Student
// - firstName: “Bruce”
// - lastName: “Wayne”
// - studentId: “S003”
因此,我们使用Decodable协议成功解码并扁平化了带有动态键的JSON。 🥳
在下一节中,让我们更进一步,改进DecodedArray结构的功能和可重用性。
支持定制集合
如果您仔细研究DecodedArray结构,它基本上只是Student数组的包装器。 这使得它成为转换为定制集合的完美候选者。
通过转换为定制集合,DecodedArray结构可以利用数组字面量以及过滤和映射等所有标准集合功能。
首先,让我们定义一个 typealias类型别名来表示Student数组,并相应地更新DecodedArray的另一部分。 当我们稍后使DecodedArray符合Collection协议时,需要typealias类型别名。
这是更新的DecodedArray,我用***标记了所做的更改。
struct DecodedArray: Decodable {
// ***
// Define typealias required for Collection protocl conformance
typealias DecodedArrayType = [Student]
// ***
private var array: DecodedArrayType
// Define DynamicCodingKeys type needed for creating decoding container from JSONDecoder
private struct DynamicCodingKeys: CodingKey {
// Use for string-keyed dictionary
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
init(from decoder: Decoder) throws {
// Create decoding container using DynamicCodingKeys
// The container will contain all the JSON first level key
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
// ***
var tempArray = DecodedArrayType()
// Loop through each keys in container
for key in container.allKeys {
// Decode Student using key & keep decoded Student object in tempArray
let decodedObject = try container.decode(Student.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
// Finish decoding all Student objects. Thus assign tempArray to array.
array = tempArray
}
}
接下来,让我们扩展DecodedArray并符合Collection协议。
extension DecodedArray: Collection {
// Required nested types, that tell Swift what our collection contains
typealias Index = DecodedArrayType.Index
typealias Element = DecodedArrayType.Element
// The upper and lower bounds of the collection, used in iterations
var startIndex: Index { return array.startIndex }
var endIndex: Index { return array.endIndex }
// Required subscript, based on a dictionary index
subscript(index: Index) -> Iterator.Element {
get { return array[index] }
}
// Method that returns the next index when iterating
func index(after i: Index) -> Index {
return array.index(after: i)
}
}
遵守Collection协议的细节超出了本文的范围。如果你想了解更多,我强烈推荐这篇很棒的文章。
仅此而已,我们已经完全将DecodedArray结构转换为定制集合。
再一次,让我们在Xcode playground 中测试我们的更改。但这次我们从Collection协议一致性中获得了一些很酷的功能——数组字面量、map和filter 。
let jsonData = Data(jsonString.utf8)
let decodedResult = try! JSONDecoder().decode(DecodedArray.self, from: jsonData)
// Array literal
dump(decodedResult[2])
//▿ __lldb_expr_5.Student
//- firstName: “Bruce”
//- lastName: “Wayne”
//- studentId: “S003”
// Map
dump(decodedResult.map({ $0.firstName }))
// Output:
//▿ 3 elements
//- “Tony”
//- “Peter”
//- “Bruce”
// Filter
dump(decodedResult.filter({ $0.studentId == “S002” }))
// Output:
//▿ __lldb_expr_1.Student
//- firstName: “Peter”
//- lastName: “Parker”
//- studentId: “S002”
很酷,不是吗?只需稍加点努力,让我们通过范型化DecodedArray使其更酷,以便我们可以在其他对象类型上重用它。
范型化DecodedArray,增加可重用性
要使我们的DecodedArray泛型化,我们只需要添加一个泛型参数子句,并将所有Student类型替换为占位符类型T。
我再次用***标记了所有更改。
// ***
// Add generic parameter clause
struct DecodedArray<T: Decodable>: Decodable {
// ***
typealias DecodedArrayType = [T]
private var array: DecodedArrayType
// Define DynamicCodingKeys type needed for creating decoding container from JSONDecoder
private struct DynamicCodingKeys: CodingKey {
// Use for string-keyed dictionary
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
init(from decoder: Decoder) throws {
// Create decoding container using DynamicCodingKeys
// The container will contain all the JSON first level key
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var tempArray = DecodedArrayType()
// Loop through each keys in container
for key in container.allKeys {
// ***
// Decode T using key & keep decoded T object in tempArray
let decodedObject = try container.decode(T.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
// Finish decoding all T objects. Thus assign tempArray to array.
array = tempArray
}
}
有了这一切,我们现在可以用它来解码任何对象类型。为了在实践中看到这一点,让我们使用我们支持范型的DecodedArray来解码以下JSON。
{
“Vegetable”: [
{ “name”: “Carrots” },
{ “name”: “Mushrooms” }
],
“Spice”: [
{ “name”: “Salt” },
{ “name”: “Paper” },
{ “name”: “Sugar” }
],
“Fruit”: [
{ “name”: “Apple” },
{ “name”: “Orange” },
{ “name”: “Banana” },
{ “name”: “Papaya” }
]
}
上面的JSON表示按类别分组的Food对象数组。 因此,我们必须首先定义Food结构。
struct Food: Decodable {
let name: String
let category: String
enum CodingKeys: CodingKey {
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Decode name
name = try container.decode(String.self, forKey: CodingKeys.name)
// Extract category from coding path
category = container.codingPath.first!.stringValue
}
}
定义Food结构后,我们现在准备使用范型化的DecodedArray解码给定的JSON。
let jsonString = “””
{
“Vegetable”: [
{ “name”: “Carrots” },
{ “name”: “Mushrooms” }
],
“Spice”: [
{ “name”: “Salt” },
{ “name”: “Paper” },
{ “name”: “Sugar” }
],
“Fruit”: [
{ “name”: “Apple” },
{ “name”: “Orange” },
{ “name”: “Banana” },
{ “name”: “Papaya” }
]
}
“””
let jsonData = Data(jsonString.utf8)
// Define DecodedArray type using the angle brackets (<>)
let decodedResult = try! JSONDecoder().decode(DecodedArray<[Food]>.self, from: jsonData)
// Perform flatmap on decodedResult to convert [[Food]] to [Food]
let allFood = decodedResult.flatMap{ $0 }
dump(allFood)
// Ouput:
//▿ 9 elements
//▿ __lldb_expr_11.Food
// - name: “Apple”
// - category: “Fruit”
//▿ __lldb_expr_11.Food
// - name: “Orange”
// - category: “Fruit”
//▿ __lldb_expr_11.Food
// - name: “Banana”
// - category: “Fruit”
//▿ __lldb_expr_11.Food
// - name: “Papaya”
// - category: “Fruit”
//▿ __lldb_expr_11.Food
// - name: “Salt”
// - category: “Spice”
//▿ __lldb_expr_11.Food
// - name: “Paper”
// - category: “Spice”
//▿ __lldb_expr_11.Food
// - name: “Sugar”
// - category: “Spice”
//▿ __lldb_expr_11.Food
// - name: “Carrots”
// - category: “Vegetable”
//▿ __lldb_expr_11.Food
// - name: “Mushrooms”
// - category: “Vegetable”
请注意,decodedResult是Food数组([[Food]])的数组。因此,为了获得Food对象([Food])的数组,我们在解码结果上应用flatMap来将[[Food]]转换为[Food]。
小结
本文仅演示了2层的解码和扁平化JSON,您绝对可以在3层或更多层的JSON上应用相同的概念。我会把它留给你作为练习!
如果您想在Xcode playground上尝试解码方法,以下是完整的示例代码。
你觉得这种解码方法怎么样?请随时在下面的评论部分留下您的评论或想法。
如果您喜欢这篇文章,请务必查看我创作的其他与Swift相关的文章。
您也可以在推特上关注我,以获取更多与iOS开发相关的文章。
感谢您的阅读。🧑🏻💻