本文的主要内容来自于https://benscheirman.com/2017/06/swift-json.html,并在此基础上进行了翻译和精简。更多细节可以直接阅读原文。
本文中的Swift版本为5.8
快速开始
有些朋友不喜欢长篇大论的理论和原理讲解,想快速掌握大概的使用方法,所以我在这里举了两个例子,以满足这方面的需求。
/*
编码JSON字符串
*/
struct Person: Codable {
var name: String
var age: Int32
var height: Double
var email: String
}
let person = Person(name: "张三", age: 23, height: 1.78,email: "12345@qq.com")
// 创建json编码器
let encoder = JSONEncoder()
// 友好输出,即包含换行和缩进
encoder.outputFormatting = .prettyPrinted
do {
// 编码
let jsonData = try encoder.encode(person)
let jsonString = String(data: jsonData, encoding: .utf8)
if jsonString != nil {
print(jsonString!)
}
} catch {
print(error.localizedDescription)
}
/*
解码JSON字符串
*/
struct Person: Codable {
var name: String
var age: Int32
var height: Double
var email: String
}
let jsonString = """
{"age":23,"email":"12345@qq.com","name":"张三","height":1.78}
"""
let decoder = JSONDecoder()
// 创建json解码器
let jsonData = jsonString.data(using: .utf8)
do {
// 解码
let person = try decoder.decode(Person.self, from: jsonData!)
print(person)
} catch {
print(error.localizedDescription)
}
如果上面的例子不能解决你的问题,那么我建议全面系统地学习json字符串的转换,正所谓磨刀不负砍柴工。
简单例子
这次在结构体中加入枚举类型,然后将其编码成json字符串。
enum Hobby: String, Codable {
case painting
case reading
case swimming
}
// 如果支持编码和解码,则必须遵循Codable协议
struct Student: Codable {
var name: String
var email: String
var hobby: Hobby
}
let stu1 = Student(name: "张三", email: "zhangsan@qq.com", hobby: .painting)
let encoder = JSONEncoder()
do {
// 因为编解码可能会失败,所以必须要使用try,并处理发生失败的情况
let dataString = try encoder.encode(stu1)
let jsonString = String(data: dataString, encoding: .utf8)
print(jsonString!)
} catch {
print(error.localizedDescription)
}
上面是将struct编码成json字符串的过程,解码json字符串到struct的流程与上面类似,可尝试自己编写。
自定义字段名
有时候我们并不想把变量名称或常量名称当作json字符串中的字段名,那么可以重新定义CodingKeys
枚举,达到自定义json字段名的效果。例子如下。
struct Student: Codable {
var name: String
var email: String
// 需要遵循CodingKey协议
enum CodingKeys: String, CodingKey {
case name = "Name"
case email = "Email"
}
}
let stu = Student(name: "张三", email: "zhangsan@qq.com")
let encoder = JSONEncoder()
do {
let jsonData = try encoder.encode(stu)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString!)
} catch {
print(error.localizedDescription)
}
处理日期
JSON中并没有对应的日期类型,如果直接编码的话,会输出一串数字,而不是相应的日期格式,所以在编码之前,可以设置编码器中的日期格式。
struct Foo: Codable {
var date: Date
}
let foo = Foo(date: Date.now)
let encoder = JSONEncoder()
// 设置日期格式
encoder.dateEncodingStrategy = .iso8601
let jsonData = try! encoder.encode(foo)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString!)
当然,如果上面不满足需求,那么可以进一步自定义。
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd HH:mm:ss"
encoder.dateEncodingStrategy = .formatted(df)
如果还是不能满足需求,那么还可以使用.custom
,具体使用方法请查阅相关文档。
处理URL
假设有如下json字符串。
{
"title": "百度一下",
"url": "http://baidu.com"
}
那么对应的struct
为如下。
struct Webpage: Codable {
var title: String
var url: URL
}
解码过程也是相当的简单,如下所示。
let jsonString = """
{
"title": "百度一下",
"url": "http://baidu.com"
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
var page = try! decoder.decode(Webpage.self, from: jsonData!)
处理顶层数组
假设有如下json字符串。
[{"name":"张三"},{"name":"李四"},{"name":"王五"}]
解码过程如下。
struct Article: Codable {
var title: String
}
let articlesString = """
[{"title": "标题1"}, {"title":"标题2"}, {"title":"标题3"}]
"""
let data = articlesString.data(using: .utf8)
let decoder = JSONDecoder()
// 注意这里的类型写法,数组中的元素需要遵循Codable协议
var articles = try! decoder.decode([Article].self, from: data!)
for article in articles {
print(article.title)
}
更复杂的结构
有些复杂的json字符串可以由简单的结构组成,所以在定义这些json结构时,可以使用嵌套完成复杂json的结构体的定义。如下面的例子。
{
"meta": {
"page": 1,
"total_pages": 4,
"per_page": 10,
"total_records": 38
},
"breweries": [
{
"id": 1234,
"name": "Saint Arnold"
},
{
"id": 52892,
"name": "Buffalo Bayou"
}
]
}
struct PagedBreweries : Codable {
struct Meta : Codable {
let page: Int
let totalPages: Int
let perPage: Int
let totalRecords: Int
enum CodingKeys : String, CodingKey {
case page
case totalPages = "total_pages"
case perPage = "per_page"
case totalRecords = "total_records"
}
}
struct Brewery : Codable {
let id: Int
let name: String
}
let meta: Meta
let breweries: [Brewery]
}
自定义编解码
上面例子中的编解码过程都不需要我们干预,非常方便我们使用,但如果想更精准的控制编解码过程,那么就需要自定义编解码过程。
编码
这里以车为例子,里面包含一些基本信息,有些信息没有实际意义,仅仅为了演示需要。
enum CarColor: String, Codable {
case white
case black
case red
case blue
}
struct Car: Codable {
var owner: String
var manufacturer: String
var price: String
var color: CarColor
// 无任何意义,仅用于演示
var sizes: [Double]
}
然后对车进行扩展,实现encode
方法。
extension Car {
enum CodingKeys: String, CodingKey {
case owner
case manufacturer
case price
case color
case sizes
}
func encode(to encoder: Encoder) throws {
// 获取container,具体解释见后面
var container = encoder.container(keyedBy: CodingKeys.self)
do {
// 加入编码数据,第一项为数据的值,第二项为字段的值
try container.encode(owner, forKey: .owner)
try container.encode(manufacturer, forKey: .manufacturer)
try container.encode("\(price)¥", forKey: .price)
try container.encode(color, forKey: .color)
// 获取第二种类型的container,准备对数组进行自定义编码过程
var sizesContainer = container.nestedUnkeyedContainer(forKey: .sizes)
for size in sizes {
// 自定义编码数组过程,将每个数组元素翻倍
try sizesContainer.encode(size * size)
}
} catch {
print(error.localizedDescription)
}
}
}
let car = Car(owner: "张三", manufacturer: "比亚迪", price: "336541.23", color: .red, sizes: [1.0, 2.0, 3.0])
let encoder = JSONEncoder()
let jsonData = try! encoder.encode(car)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString!)
上面代码中出现了新的事物,即container
。什么是container
?原文中并没有进行解释,可以简单理解为管理编码数据的对象。
container
有三种类型。
- 有对应键值的container,本质上是字典,最常用的类型。
- 无对应键值的container,如数组。
- 单一值container,不带任何类型的元素的原始值。
在上面代码中,注意container必须是可变属性,需要使用var声明。
解码
解码是编码的逆操作,大部分操作相似,下面例子是对车信息的json字符串进行解码。
let jsonString = """
{"sizes":[1,2,3],"manufacturer":"比亚迪","owner":"张三","price":"336541.23¥","color":"red"}
"""
// 扩展car,实现解码操作
extension Car {
init(from decoder: Decoder) throws {
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
let owner = try container.decode(String.self, forKey: .owner)
let manufacturer = try container.decode(String.self, forKey: .manufacturer)
let suffixPrice = try container.decode(String.self, forKey: .price)
// 去除价格后面的人民币标识
let price = String(suffixPrice[..<(suffixPrice.firstIndex(of: "¥") ?? suffixPrice.endIndex)])
let color = try container.decode(CarColor.self, forKey: .color)
// 准备解码数组
var tempSizes: [Double] = []
// 自定义解码数组数据
var sizesArray = try container.nestedUnkeyedContainer(forKey: .sizes)
while (!sizesArray.isAtEnd) {
let size = try sizesArray.decode(Double.self)
tempSizes.append(size)
}
self.init(owner: owner, manufacturer: manufacturer, price: price, color: color, sizes: tempSizes)
} catch {
print(error.localizedDescription)
self.init(owner: "", manufacturer: "", price: "", color: .black, sizes: [])
}
}
}
let decoder = JSONDecoder()
let jsonData = jsonString.data(using: .utf8)
let car = try! decoder.decode(Car.self, from: jsonData!)
print(car)
处理继承关系
class Person : Codable {
var name: String?
}
class Employee : Person {
var employeeID: String?
}
如果对Employee编码的话,会出现什么情况?答案是只会编码Person中的属性。为此,我们需要在各自的类中自定义编码过程,并实现相关调用。
class Person : Codable {
var name: String?
private enum CodingKeys : String, CodingKey {
case name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
}
}
class Employee : Person {
var employeeID: String?
private enum CodingKeys : String, CodingKey {
case employeeID = "emp_id"
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder) // 试试不加上这个语句会出现什么情况
var container = encoder.container(keyedBy: CodingKeys.self)
// try super.encode(to: container.superEncoder())
try container.encode(employeeID, forKey: .employeeID)
}
}
let employee = Employee()
employee.name = "张三"
employee.employeeID = "000001"
let encoder = JSONEncoder()
let jsonData = try! encoder.encode(employee)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString!)
上面编码后的结果如下。
{
"name": "张三",
"emp_id": "000001"
}
虽然属性都编码成功了,但是结果是扁平的,我们想改为像继承一样具有层级的json字符串,那么将try super.encode(to: encoder)
改为try super.encode(to: container.superEncoder())
并放在定义container的后面就可以了,编码结果如下。
{
"super": {
"name": "张三"
},
"emp_id": "000001"
}
上面结果虽然是我们想要的,但是还差点意思,主要原因在于super不是我们想要的名字,我们想要的是person。
class Employee : Person {
var employeeID: String?
private enum CodingKeys : String, CodingKey {
case employeeID = "emp_id"
// 增加person
case person
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// 指定键值
try super.encode(to: container.superEncoder(forKey: .person))
try container.encode(employeeID, forKey: .employeeID)
}
}
总结
本文只介绍了我经常遇到的几种情况下的使用方法,更多使用技巧和方法请阅读原文和官方文档,
网友评论