Swift中的JSON数据解析

4,619 阅读5分钟

本文原载于我的博客:www.seekingmini.top/archives/sw…

0 写在前面

平时写小项目时请求JSON格式的数据,怎么解析向来是一个大问题,尤其碰到嵌套型的数据,更是不知道怎么写代码才好,所以写一篇教程来记录一下。刚好好久也没写博客了,赶快写一篇来刷刷存在感...

1 JSON是什么

JSON是一个以文字为主、轻量型,并且容易使用来储存以及交换资料的方式。它通常用来呈现结构性的数据,许多我每天使用的网页服务都是以JSON格式为主的API。

一句话概括,一个JSON对象,就是一个个的<key, value>对的集合{<key, value>})。

  • 举例
{
	"name": "SeekingMini",
	"age": 21,
	"hobby": ["Swimming", "Coding"],
	"location": {
		"country": "China",
		"city": "Suzhou"
	}
}

在这个例子中,最外层的{}包含了4个key,分别是nameagehobbylocation,它们都是String类型;而它们对应value的类型分别是StringInt[String]JSON对象。我们发现,key的类型一定是String即字符串,而value的类型是任意的。

正是因为value的类型可以是任何类型,如果valueJSON对象,这意味着出现了嵌套,如location对应的value就是一个嵌套在JSON对象里的JSON对象,这为我们解析JSON数据增加了复杂度。上面的例子可以用下面的结构图表示:

|-- TOP
  |-- "name" -> "SeekingMini"
  |-- "age" -> 21
  |-- "hobby" -> ["Swimming", "Coding"]
  |-- "location"
    |-- "country" -> "China"
    |-- "city" -> "Suzhou"

把它旋转90度来看,其实就是一个N叉树。根结点忽略不计,其他的非叶子结点都是key,而叶子结点都是value。当然了,如果没有嵌套,那么这棵树只有3层,顶层是TOP,表示最外层的大括号;第2层是key;第3层是value

但是出现了嵌套,location的子树就是它的value,而这棵子树自身是一个JSON对象,它的叶子结点就是非JSON对象类型的value

还是不清楚什么是JSON的话,就参考菜鸟教程吧!我们的重头戏放在如何解析包含嵌套的JSON数据。

2 解析JSON数据

解析一个JSON格式的数据,最重要的是什么?我认为应该是结构层次。我们只有定义了准确的结构层次,才能在每一层解析出正确的数据。

1) 无脑struct叠加

还是以上面的数据为例,我们可以定义出这样的结构体:

struct Person: Codable {
    var name: String
    var age: Int
    var hobby: [String]
    var location: Location  
}

extension Person {
    struct Location: Codable {
        var country: String
        var city: String
    }
}

稍微说明一下:Person这个结构体就代表了整个JSON对象的结构。而Person中的每个属性代表key;属性值代表value。由于存在嵌套,所以location这个keyvalue需要重新定义一个结构体Location。这样的话就能解析了。解析代码如下:

// json data
let jsonData = """
{
"name": "SeekingMini",
"age": 21,
"hobby": ["Swimming", "Coding"],
"location": {
"country": "China",
"city": "Suzhou"
}
}
"""

// parse
if let data = jsonData.data(using: .utf8) {
    let decoder = JSONDecoder()
    if let person = try? decoder.decode(Person.self, from: data) {
      	print(person)
        print(person.name)
        print(person.age)
        print(person.hobby)
        print(person.location.country)
        print(person.location.city)
    }
}

// 打印结果如下:
// Person(name: "SeekingMini", age: 21, hobby: ["Swimming", "Coding"], location: // __lldb_expr_45.Person.Location(country: "China", city: "Suzhou"))
// SeekingMini
// 21
// ["Swimming", "Coding"]
// China
// Suzhou

struct的叠加非常简单粗暴,但是会有问题,比如说我的结构体定义成这样:

struct Person: Codable {
    var name: String
    var age: Int
    var hobby: [String]
  	// 比如说我不定义嵌套的Location结构体,而直接在外层定义country和city
    var country: String
  	var city: String
}

那么location这个key是解析不出来的,更别提countrycity了。我们应该怎么办呢?

2) 使用enumCodingKey协议

利用enum定义JSON对象的结构:

enum CodingKeys: String, CodingKey {
  
    // 最外层的key(把需要用到的定义出来即可)
    case name
    case age
    case hobby
    case location
    
    enum LocationKeys: String, CodingKey {
        // location层对应的key
        case country
        case city
    }
}

nameagehobbylocation这4个key在同一层,而countrycity在下一层。每一层就是一个enum。通过这样的定义,我们可以写出以下的代码来解析:

init(from decoder: Decoder) throws {
        // 处理name、age和hobby的解码
        let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
        name = try rootContainer.decode(String.self, forKey: .name)
        age = try rootContainer.decode(Int.self, forKey: .age)
        hobby = try rootContainer.decode([String].self, forKey: .hobby)
        
        // 处理country和city的解码
        let locationContainer = try rootContainer.nestedContainer(keyedBy: CodingKeys.LocationKeys.self, forKey: .location)
        country = try locationContainer.decode(String.self, forKey: .country)
        city = try locationContainer.decode(String.self, forKey: .city)
    }

decoder使用 container(keyedBy:)获取顶级容器后, 我们可以重复使用这些方法:

  • nestedContainer(keyedBy:forKey:):从给定键的对象获取嵌套对象
  • nestedUnkeyedContainer(forKey:):从给定键的对象获取嵌套数组
  • nestedContainer(keyedBy:):从数组中获取下一个嵌套对象
  • nestedUnkeyedContainer():从数组中获取下一个嵌套数组

完整代码如下:

import Foundation

// json data
let jsonData = """
{
"name": "SeekingMini",
"age": 21,
"hobby": ["Swimming", "Coding"],
"location": {
        "country": "China",
        "city": "Suzhou"
    }
}
"""

struct Person {
    
    var name: String
    var age: Int
    var hobby: [String]
    var country: String
    var city: String
    
}

extension Person: Decodable {
    
    enum CodingKeys: String, CodingKey {
        // 最外层的key
        case name
        case age
        case hobby
        case location
        
        enum LocationKeys: String, CodingKey {
            // location层对应的key
            case country
            case city
        }
        
    }
    
    init(from decoder: Decoder) throws {
        // 处理name、age和hobby的解码
        let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
        name = try rootContainer.decode(String.self, forKey: .name)
        age = try rootContainer.decode(Int.self, forKey: .age)
        hobby = try rootContainer.decode([String].self, forKey: .hobby)
        
        // 处理country和city的解码
        let locationContainer = try rootContainer.nestedContainer(keyedBy: CodingKeys.LocationKeys.self, forKey: .location)
        country = try locationContainer.decode(String.self, forKey: .country)
        city = try locationContainer.decode(String.self, forKey: .city)
    }
    
}

// parse
if let data = jsonData.data(using: .utf8) {
    let decoder = JSONDecoder()
    if let person = try? decoder.decode(Person.self, from: data) {
        print(person)
    }
}

// 打印结果
// Person(name: "SeekingMini", age: 21, hobby: ["Swimming", "Coding"], country: "China", city: "Suzhou")

使用enumCodingKey协议有一个好处:对于Struct的定义不用考虑JSON对象的结构,而JSON对象的结构都是通过enum的嵌套来定义的。

参考

如何使用Swift Decodable协议解码嵌套的JSON结构?

《iOS 13 App程式設計進階攻略》