这是我参与更文挑战的第14天,活动详情查看: 更文挑战
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.
typealias Codable = Decodable & Encodable
protocol Encodable {
func encode(to encoder: Encoder) throws
}
protocol Decodable {
init(from decoder: Decoder) throws
}
可以看到它实际上是 Decodable 和 Encodeable 的复合接口,用作数据的解析和编码;Swift4.0标准库中正式引入了Codable接口;
先简单看一个Model to JSON 的例子,感受下官方Codable的逻辑
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let banner = GroceryProduct(name: "香蕉", points: 250, description: "海南产的")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
/// 对banner 实例进行JSON编码
let data = try encoder.encode(banner)
print(String(data: data, encoding: .utf8)!)
/* 打印:
{
"name" : "Pear",
"points" : 250,
"description" : "A ripe pear."
}
*/
/// 对 JSON String 转Model 实例
let json = """
{
"name": "榴莲",
"points": 600,
"description": "一般人很难接受的水果"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let product = try decoder.decode(GroceryProduct.self, from: json)
print(product.name) // 打印 "榴莲"
过滤编码属性
在json的序列化中可以过滤掉部分的字段,只需要声明一个 CodingKeys 枚举属性
struct GroceryProduct: Codable {
var name: String
var points: Int = 0
var description: String?
enum CodingKeys: String, CodingKey {
case name
case description
}
}
输出结果
{"name":"香蕉","description":"海南产的"}
键值修改
有时候服务下发的字段名不是我们想要的,比如接口表示性别使用的是gender,而有些接口使用确实sex,为了适应不用的接口,也可以通过CodingKeys 改变编码属性的名称
struct GroceryProduct: Codable {
var name: String
var points: Int = 0
var description: String?
enum CodingKeys: String, CodingKey {
case name
case description = "desc"
}
}
输出结果:
{"name":"香蕉","desc":"海南产的"}
键值策略自定义
在日常使用,我们总会碰到不同的命名规则,例如下划线命名(person_name)和驼峰命名(personName)以及首字母大写的帕斯卡命名(PersonName),假设你和后端并没有统一命名风格,那么自定义键值或许还能弥补一下。
第一种方式是直接在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)
}
// 对person实例进行Json编码
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = 自定义
let data = try! encoder.encode(person)
let personEncodedString = String(data: data, encoding: .utf8)!
print(personEncodedString)
结构自定义
struct Person: Codable {
var gender: String
var age: Int
var name: String
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)
// nestedContainer 解析name属性
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)
//nestedContaine 解析name节点
var name = container.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
try name.encode(firstName, forKey: .firstName)
try name.encode(secondName, forKey: .secondName)
}
}
三方JSON解析库
在OC中,我们有很多优秀的第三方库帮助我们实现,比如MJExtension、JSONModel等,这些库基本都是利用runtime实现读取属性名并利用kvc重新赋值属性。
在Swift中,由于runtime的局限,比较出名的有SwiftyJSON、ObjectMapper、HandyJSON等。
其中
1、SwiftyJSON本质上仍然是根据JSON结构去取值,使用起来顺手、清晰;
2、ObjectMapper实现了JSON直接转Model的功能,不过使用起来,代码量会多一点,因为我们必须遵循Mappable协议,制定json内的每一个key和model属性的对应关系。
3、 HandyJSON
另辟蹊径,采用Swift反射
+内存赋值
的方式来构造Model实例,保持原汁原味的Swift类定义。
HandyJSON
// 假设这是服务端返回的统一定义的response格式
class BaseResponse<T: HandyJSON>: HandyJSON {
var code: Int? // 服务端返回码
var data: T? // 具体的data的格式和业务相关,故用泛型定义
public required init() {}
}
// 假设这是某一个业务具体的数据格式定义
struct SampleData: HandyJSON {
var id: Int?
}
let sample = SampleData(id: 2)
let resp = BaseResponse<SampleData>()
resp.code = 200
resp.data = sample
let jsonString = resp.toJSONString()! // 从对象实例转换到JSON字符串
print(jsonString) // print: {"code":200,"data":{"id":2}}
if let mappedObject = JSONDeserializer<BaseResponse<SampleData>>.deserializeFrom(json: jsonString) { // 从字符串转换为对象实例
print(mappedObject.data?.id)
}
- HandyJSON支持 JSON直接转Model,定义class时,有两点注意:
- 必须遵循HandyJSON协议
- 需要实现空的initializer (当然Struct结构体 可以不需要init())
-
HandyJSON还支持Struct,使用方式与Class基本一致
-
HandyJSON支持枚举,只需要enum构造时服从
HandyJSONEnum
协议即可。enum AnimalType: String, HandyJSONEnum { case Cat = "cat" case Dog = "dog" case Bird = "bird" } struct Animal: HandyJSON { var name: String? var type: AnimalType? } let jsonString = "{\"type\":\"cat\",\"name\":\"Tom\"}" if let animal = Animal.deserialize(from: jsonString) { print(animal.type?.rawValue) }
-
此外,HandyJSON还支持一些非基础类型、复杂类型,包括嵌套结构,如可选、隐式解包可选、集合等
-
HandyJSON支持指定从哪个具体路径开始解析,反序列化到Model。
class Cat: HandyJSON { var id: Int64! var name: String! required init() {} } let jsonString = "{\"code\":200,\"msg\":\"success\",\"data\":{\"cat\":{\"id\":12345,\"name\":\"Kitty\"}}}" ///其中,直接通过 designatedPath 定位到我们需要的节点处。 if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString, designatedPath: "data.cat") { print(cat.name) }
-
HandyJSON支持有继承关系的Model类,就是说及时某个类没有实现HandyJSON协议,只要父类有实现,依然可以转化model。
-
HandyJSON
还支持对象转字典、对象转模型。class BasicTypes: HandyJSON { var int: Int = 2 var doubleOptional: Double? var stringImplicitlyUnwrapped: String! required init() {} } let object = BasicTypes() object.int = 1 object.doubleOptional = 1.1 object.stringImplicitlyUnwrapped = “hello" print(object.toJSON()!) // 序列化到字典 print(object.toJSONString()!) // 序列化到JSON字符串 print(object.toJSONString(prettyPrint: true)!) // 序列化为格式化后的JSON字符串
总结
对于简单的数据解析,直接使用Codable 就足以
如果是大型项目或有大量复杂JSON对象,以及有自定义相关需求比较多,那建议使用 HandyJSON