使用Codable解析JSON
5个月前 • 250次点击 • 来自 Swift
笔者最近接触最多的场景就是JSON解析,虽然使用开源包大大简化了序列化和反序列化的操作,通过查阅资料还是了解到Codable,Swift4.0标准库中正式引入了Codable接口:
/// A type that can convert itself into and out of an external representation.
///
/// `Codable` is a type alias for the `Encodable` and `Decodable` protocols.
/// When you use `Codable` as a type or a generic constraint, it matches
/// any type that conforms to both protocols.
public typealias Codable = Decodable & Encodable
可以看到它实际上是 Decodable & Encodable 的复合接口,用作数据的解析和编码,先使用一个实例来演示下Codable的使用:
struct Person: Codable {
var name: String
var gender: String
var age: Int
}
let person = Person(name: "devler", gender: "male", age: 18)
//对person实例进行Json编码
let encoder = JSONEncoder()
let data = try! encoder.encode(person)
let personEncodedString = String(data: data, encoding: .utf8)!
print(personEncodedString)
//对Json数据进行解码转换成person实例
let decoder = JSONDecoder()
let personDecoded = try! decoder.decode(Person.self, from: data)
print(personDecoded)
输出为:
{"name":"devler","age":18,"gender":"male"}
Person(name: "devler", gender: "male", age: 18)
1.过滤编码属性
在Json的序列化中可以过滤掉部分字段,只需要声明一个 CodingKeys 枚举属性:
struct Person: Codable {
var name: String
var gender: String
//age属性没有在 CodingKeys 中声明,不会被编码
var age: Int
enum CodingKeys: String, CodingKey {
case name
case gender
}
}
输出结果:
{"name":"devler","gender":"male"}
2.键值修改
例如,在不同程序员开发的接口下,有些接口表示性别使用的是gender,而有些接口使用的却是sex,为了适应不同的接口,那么你可以CodingKeys中改变编码属性的名称:
struct Person: Codable {
var name: String
var gender: String
//age属性没有在 CodingKeys 中声明,不会被编码
var age: Int
enum CodingKeys: String, CodingKey {
case name
case gender = "sex"
}
}
输出结果:
{"name":"devler","sex":"male"}
3.键值策略
在日常使用,我们总会碰到不同的命名规则,例如下划线命名(person_name)和驼峰命名(personName)以及首字母大写的帕斯卡命名(PersonName),假设你和后端并没有统一命名风格,那么自定义键值或许还能弥补一下。
struct Person: Codable {
var firstName: String
var secondName: String
}
let person = Person(firstName: "dev", secondName: "ler")
序列化后的json字符串为
{"firstName":"dev","secondName":"ler"}
假设需要将驼峰命名(personName)转换为下划线命名(person_name):
{"second_name":"ler","first_name":"dev"}
第一种方式是直接在CodingKeys直接修改:
struct Person: Codable {
var firstName: String
var secondName: String
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case secondName = "second_name"
}
}
第二种方式是使用更通用的键值转换策略来解决JSON解码和编码的键策略问题。
/// The strategy to use for automatically changing the value of keys before encoding.
public enum KeyEncodingStrategy {
...
/// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types.
case custom(([CodingKey]) -> CodingKey)
}
/// The strategy to use for automatically changing the value of keys before decoding.
public enum KeyDecodingStrategy {
...
/// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
case custom(([CodingKey]) -> CodingKey)
}
我们通过扩展JSONEncoder.KeyEncodingStrategy实现编码中的键值转换,通过扩展JSONEncoder.KeyDecodingStrategy来实现解码的键值转换。
在开始编写转换代码之前先介绍一个协议CodingKey(A type that can be used as a key for encoding and decoding.),它是被当做键 (Key) 用于编码和解码的类型。定义了以下四个方法:
protocol CodingKey : CustomDebugStringConvertible, CustomStringConvertible {
//表示Key为String
var stringValue: String { get }
init?(stringValue: String)
//表示Key为Int
var intValue: Int? { get }
init?(intValue: Int)
}
现在我们通过扩展JSONEncoder.KeyEncodingStrategy,来实现custom(([CodingKey]) -> CodingKey),最后JSONEncoder.keyEncodingStrategy就会调用**.custom**键编码策略。
//扩展String实现驼峰和下划线命名规则转换
extension String {
var camelName: String {
var result = ""
var flag = false
forEach { c in
let s = String(c)
if s == "_" {
flag = true
} else {
if flag {
result += s.uppercased()
flag = false
} else {
result += s
}
}
}
return result
}
var underscore_name: String {
var result = ""
forEach { c in
let num = c.unicodeScalars.map { $0.value }.last!
let s = String(c)
if num > 64 && num < 91 {
result += "_"
result += s.lowercased()
} else {
result += s
}
}
return result
}
}
struct SimpleCodingKey: CodingKey {
var stringValue: String
var intValue: Int?
init(stringValue: String) {
self.stringValue = stringValue
}
init(intValue: Int) {
stringValue = "\(intValue)"
self.intValue = intValue
}
}
//自定义键值转换策略 - 转为下划线命名规则
extension JSONEncoder.KeyEncodingStrategy {
static var convertToUnderlineCase: JSONEncoder.KeyEncodingStrategy {
return .custom { codingKeys in
let str = codingKeys.last!.stringValue
return SimpleCodingKey(stringValue: str.underscore_name)
}
}
}
struct Person: Codable {
var firstName: String
var secondName: String
}
let person = Person(firstName: "dev", secondName: "ler")
// 对person实例进行Json编码
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToUnderlineCase
let data = try! encoder.encode(person)
let personEncodedString = String(data: data, encoding: .utf8)!
print(personEncodedString)
4.结构自定义
例如:
struct Person: Codable {
var gender: String
var age: Int
var firstName: String
var secondName: String
}
经过序列号后,我们希望得到的层级的JSON数据:
{
"gender" : "male",
"age":18,
"name": {
"firstName" : "dev"
"secondName": "ler"
}
}
此时需要使用NameKeys来进行结构重塑,并手动的确立name与firstName,secondName之间的对应关系,我们通过重载Codable的方法来进行数据的编码和解码:
struct Person: Codable {
var gender: String
var age: Int
var firstName: String
var secondName: String
enum CodingKeys: String, CodingKey {
case gender
case age
case name
}
enum NameKeys: String, CodingKey {
case firstName
case secondName
}
}
extension Person {
//解析
init(from decoder: Decoder) throws {
let vals = try decoder.container(keyedBy: CodingKeys.self)
gender = try vals.decode(String.self, forKey: CodingKeys.gender)
age = try vals.decode(Int.self, forKey: CodingKeys.age)
let name = try vals.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
firstName = try name.decode(String.self, forKey: .firstName)
secondName = try name.decode(String.self, forKey: .secondName)
}
//编码
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(gender, forKey: .gender)
try container.encode(age, forKey: .age)
var name = container.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
try name.encode(firstName, forKey: .firstName)
try name.encode(secondName, forKey: .secondName)
}
}
let person = Person(gender: "male", age: 18, firstName: "dev", secondName: "ler")
let encoder = JSONEncoder()
let resultData = try! encoder.encode(person)
let personEncodedString = String(data: resultData, encoding: .utf8)!
print(personEncodedString)
总结
Codable简洁又牛逼