一、前言
Codable
是随 Swift 4.0 推出的,旨在取代现有的 NSCoding 协议,支持结构体、枚举和类,能将 JSON
这种弱数据类型转换成代码中使用的强数据类型。
HandyJSON
在 Codable
推出后已经不再进行维护了,而我们的项目就是依赖于 HandyJSON
来处理 JSON 的序列化
和反序列化
,所以需要逐步迁移到Codable
以避免一些错误。
本文主要介绍一些json转模型的基本概念和我使用过程中遇到的一些解析问题,在此分享一下。
二、基本概念
1. 序列化和反序列化
- 序列化:将对象转换为字节序列的过程,在传递和保存对象时,保证对象的完整性和完整性,方便在网络上传输或者保存在文件中
let responseData = try? JSONSerialization.data(withJSONObject: response)
- 反序列化:将字节序列恢复为对象的过程,重建对象,方便使用
let obj = try? JSONSerialization.jsonObject(with: data, options: .allowFragments
2. encode和decode
- encode:将自身类型编码成其他外部类型
let data = try? JSONEncoder().encode(obj)
- decode:将其他外部类型解码成自身类型
let obj = try? JSONDecoder().decode(T.self, from: data)
3. Codable
public typealias Codable = Decodable & Encodabl
实际上,Codable就是指的编码和解码协议
4. json转模型
(1) 在用原生Codable协议的时候,需要遵守协议Codable,结构体,枚举,类都可以遵守这个协议,一般使用struct
struct UserModel:Codable {
var name: String?
var age: Int?
var sex: Bool?
var name_op: String?
}
(2) json数据里面的字段和model字段不一致
解决办法:
实现 enum CodingKeys: String, CodingKey {}这个映射关系
struct UserModel:Codable {
var name: String?
var age: Int?
var sex: Bool?
var name_op: String?
var nick: String?
enum CodingKeys: String, CodingKey {
case name
case age
case sex
case name_op
case nick = "nick_name"
}
}
上述代码中,如果后台返回的字段名称叫做nick_name,但是你想用自己命名的nick,就可以用到上述枚举中的映射去替换你想要的字段名字。
注意: CodingKeys必须字段写全,并且required init(from decoder: Decoder) throws方法必须全部解析所有字段,否则不能正确解析出正确的结果。
(3) 如果你的模型里面带有嵌套关系,比如你的模型里面有个其他模型或者模型数组,那么只要保证嵌套的模型里面依然实现了对应的协议
struct UserModel:Codable {
var name: String?
var age: Int?
var sex: Bool?
var name_op: String?
var nick: String?
var books_op: [BookModel]?
enum CodingKeys: String, CodingKey {
case name
case age
case sex
case name_op
case nick = "nick_name"
}
}
// BookModel
struct BookModel:Codable {
var name:String ?
}
(4) 使用JSONDecoder进行json转model
private func jsonToModel<T: Codable>(_ modelType: T.Type, _ response: Any) -> T? {
guard let data = try? JSONSerialization.data(withJSONObject: response), let info = try? JSONDecoder().decode(T.self, from: data) else {
return nil
}
return info
}
先使用JSONSerialization进行一次序列化操作
看一下decode源码:
// MARK: - Decoding Values
/// Decodes a top-level value of the given type from the given JSON representation.
///
/// - parameter type: The type of the value to decode.
/// - parameter data: The data to decode from.
/// - returns: A value of the requested type.
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
/// - throws: An error if any value throws an error during decoding.
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {//泛型并且约束遵守协议
let topLevel: Any
do {
//反序列化操作
topLevel = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
let decoder = _JSONDecoder(referencing: topLevel, options: self.options)
//调用unbox解码并返回解码后的数据
guard let value = try decoder.unbox(topLevel, as: type) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
}
return value
}
可以看到在转model的时候,先进行一次序列化操作,decode内部又进行一次反序列化操作,苹果这样设计估计是在参数传递的时候想让我们传递字节流
至此就可以使用swift原生协议Codable进行json转model了
5. JSON字符串转模型
这是一个字符串
let jsonString:String = """
{
"name":"Tomas",
"age":"10",
"gender":"man"
}
"""
如果将它解析成User对象
let jsonData: Data = jsonString.data(using:String.Encoding.utf8)!
let decoder = JSONDecoder()
do {
let user:UserModel = try decoder.decode(UserModel.self,from:jsonData)
} catch {
}
6. JSONSerialization
Serialization 是序列化的意思,JSONSerialization
顾名思义是对JSON进行序列化。
JSONSerialization
是对 JSON 字符串进行序列化和反序列化的工具类。用这个类可以将JSON转成对象,也可以将对象转成JSON。
let dict:[String:Any] = ["name":"jack",
"age":10,
"gender":"man"]
if JSONSerialization.isValidJSONObject(dict) == false {
return
}
let data:Data = try! JSONSerialization.data(withJSONObject: dict, options: .fragmentsAllowed);
// 将data转换成制定model
guard let Model:UserModel = try? JSONDecoder().decode(UserModel.self, from: data) else {
return
}
// 将data转成字符串输出
let string = String(data: data, encoding: String.Encoding.utf8)
print(string)
print(Model.name)
Optional("{\"name\":\"jack\",\"gender\":\"man\",\"age\":10}")
Optional("jack")
- JSON转字典
let jsonString = "{\"name\":\"jack\",\"age\":10}";
let data = jsonString.data(using: .utf8)
let dict = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments)
print(dict)
三、Codable 解码设置默认值
References:
Swift - Codable 解码设置默认值
四、常见问题
1. 类型的某个属性有默认值,后台返回的JSON
没有这个属性对应的数据
(1) 正常的Demo
假设我们有一个User
类型,有一个id
属性,和一个是否被当前用户关注的属性isFollowedByCurrentUser
,并实现了Codable
协议,代码如下:
struct User: Codable {
var id: String
var isFollowedByCurrentUser: Bool?
enum CodingKeys: String, CodingKey {
case id
case isFollowedByCurrentUser = "followed"
}
}
我们的JSON
数据如下:
let jsonString = """
{
"id":"efa41bae-25fa-428b-99c1-6d3c1b178875",
"followed": true
}
"""
用JSONDecoder
进行解码:
let decoder = JSONDecoder()
let data = jsonString.data(using: .utf8)!
do {
let user = try decoder.decode(User.self, from: data)
print(user)
} catch {
print("error: \(error)")
}
毫无疑问,上面的代码是可以解码成功的。
(2) 失败的Demo
有些时候,后台返回的JSON数据可能缺少某些字段,假设缺少了followed
,那么现在的JSON数据为:
let jsonString = """
{
"id":"efa41bae-25fa-428b-99c1-6d3c1b178875"
}
"""
这时我们用上面的JSONDecoder
进行解码,也是可以解码成功的,只不过isFollowedByCurrentUser
的值为nil
而已。
现在问题来了,我们看回User类型。通常我们在某个类型添加一个Bool属性时,一般会给他一个默认值false
,所以我们会习惯的把User
写成:
struct User: Codable {
var id: String
var isFollowedByCurrentUser = false
enum CodingKeys: String, CodingKey {
case id
case isFollowedByCurrentUser = "followed"
}
}
这时如果我们再用JSONDecoder
把缺少followed
字段的JSON数据转成User
的话,是无法转成功的,错误如下:
error: keyNotFound(CodingKeys(stringValue: “followed”, intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: “No value associated with key CodingKeys(stringValue: “followed”, intValue: nil) (“followed”).”, underlyingError: nil))
JSONDecoder
在JSON
数据中无法找到followed
对应的值。
(3) 解决办法:
我们无法保证服务器总是返回完整正确的数据,所以只能从我们客户端去解决问题。
1. 把类型的所有属性都定义为Optional
类型
这是最简单方便的方法。这样解码的时候,JSONDecoder
发现JSON
没有对应的数据,就自动把这个属性设置为nil
。
2. 实现Decodable
的初始化函数,并使用decodeIfPresent
来解码
正常情况下,我们定义了CodingKeys
之后,不需要手动实现init(from decoder: Decoder) throws
这个初始化函数的,JSONDecoder
就可以正常解码。但是我们把isFollowedByCurrentUser
定义成一个非可选类型
,我们必须实现这个初始化函数,才能正常解码。明确这一点非常重要!!!
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
isFollowedByCurrentUser = try container.decodeIfPresent(Bool.self, forKey: .isFollowedByCurrentUser) ?? false
}