简单说明
在OC中,以及Swift4.0之前,系统一直没有一套数据解析的方法。在Swift4.0后,终于推出了Codable协议,可实现json数据和数据模型的相互转换。
首先来看下 Codable ,它其实是一个组合协议,由 Decodable 和 Encodable 两个协议组成。
/// A type that can convert itself into and out of an external representation.
public typealias Codable = Decodable & Encodable
/// A type that can encode itself to an external representation.
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
/// A type that can decode itself from an external representation.
public protocol Decodable {
public init(from decoder: Decoder) throws
}
Decodable 和 Encodable 分别是用来实现数据模型的解档和归档。
数据模型只要遵循了 Codable 协议,就可以方便的进行 JSON 数据和数据模型的相互转换。
使用介绍
JSON 转 模型
核心代码:
JSONDecoder().decode(type: '某类型', from: 'Data数据')
例如我们有一个个人信息的 JSON 数据,我们想要将其转换为 Person 数据模型。
let jsonString =
"""
{
"name":"LOLITA0164",
"age":26,
"address":"fuzhou"
}
"""
数据模型:
/// Persion模型,遵循 Codable 协议
class Person: Codable {
var name: String?
var age: Int?
var address: String?
}
转换过程:
// 将 json 字符串转为 data 类型
if let jsonData = jsonString.data(using: String.Encoding.utf8) {
if let person = try? JSONDecoder().decode(Person.self, from: jsonData){
// 转换成功,我们将数据输出
print(person.name!,person.age!,person.address!)
}
}
输出结果:
LOLITA0164 26 fuzhou
原理:
一旦数据模型遵循了 Codable
协议,编译器自动会生成相关编码和解码的实现。
该协议中还有一个叫 CodingKey
的协议,用来表示编码和解码的key。
protocol CodingKey {
var stringValue: String { get }
init?(stringValue: String)
var intValue: Int? { get }
public init?(intValue: Int)
}
Encoder
和 Decoder
是编码器和解码器,类似 OC 中的NSCoder。他们完成了数据的编码和解码工作。
当我们的模型遵循 Codable
时,编译器实际上帮我们完成了下面的工作:
/// Persion模型,遵循 Codable 协议
class Person: Codable {
var name: String?
var age: Int?
var address: String?
// 编码和解码的所对应的 key,编译器会自动生成成员变量的枚举形式
private enum CodingKeys: String, CodingKey {
case name = "name"
case age = "age"
case address = "address"
}
// 解码:JSON -> Model 必须实现这个方法
required init(from decoder: Decoder) throws {
// 解码器提供了一个容器,用来存储这些变量
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
address = try container.decode(String.self, forKey: .address)
}
// 编码:Model -> JSON 必须实现这个方法
func encode(to encoder: Encoder) throws {
// 编码器同样提供了一个容器,用来提供对应变量的值
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
try container.encode(address, forKey: .address)
}
}
上述是编译器自动帮我们完成遵循 Codable
协议的数据模型的编码和解码过程,这些细节部分一般不需要我们关注。但是,在有些情况下,则需要我们自行实现相应的方法。
- 数据源和模型的成员变量不一致
在实际开发过程中,经常遇到数据源和模型的成员变量不一致的情况,这种情况的出现通常是服务端和客户端未达成统一,各自有不同的想法,又或者是开发的顺序不一致,客户端先于服务端完成导致字段不统一。无论那种情况,谁去做改动都是不合理的,那么当客户端想做兼容时,就需要从 CodingKey
协议入手了。
例如服务端给了我们下面一串数据:
sonString =
"""
{
"NAME":"LOLITA0164",
"AGE":26,
"ADDRESS":"fuzhou"
}
"""
我们的数据模型依旧不变,这时我们调整一下 CodingKey
:
/// Persion模型,遵循 Codable 协议
class Person: Codable {
var name: String?
var age: Int?
var address: String?
/*
注:
1、一旦写了CodingKey,需要将所有的成员都列出来(除非你只想解析其中部分字段),并且不能重复。
2、CodingKeys是固定的枚举的名称,不能自定义。
*/
private enum CodingKeys: String, CodingKey {
case name = "NAME"
case age = "AGE"
case address = "ADDRESS"
}
}
这样,我们就可以正常解析 JSON 数据了。
- 派生类
首先看个例子:
class Dog: Codable {
var name: String?
}
class GoldenRetriever: Dog {
var age: Float?
}
派生类的数据解析:
let jsonString =
"""
{
"name":"kitty",
"age":2.5,
}
"""
if let jsonData = jsonString.data(using: String.Encoding.utf8) {
if let dog = try? JSONDecoder().decode(GoldenRetriever.self, from: jsonData){
dump(dog)
}
}
结果:
▿ JSONToModelSwift.GoldenRetriever #0
▿ super: JSONToModelSwift.Dog
▿ name: Optional("kitty")
- some: "kitty"
- age: nil
我们发现,GoldenRetriever 类的实例只解析出了父类中的 name 字段,而本类中的 age 未能解析。这说明,Codable
在继承中是无效的,当你在派生类中声明遵循该协议时,则会报错:
Redundant conformance of 'GoldenRetriever' to protocol 'Decodable'
Redundant conformance of 'GoldenRetriever' to protocol 'Encodable'
这时候,就需要我们自行实现 Codable
协议了。
class Dog: Codable {
var name: String?
}
class GoldenRetriever: Dog {
var age: Float?
private enum CodingKeys: String, CodingKey {
case name
case age
}
// 这里只实现了解码,需要编删除线格式 码时,请自行参考之前的例子
required init(from decoder: Decoder) throws {
super.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Float.self, forKey: .age)
}
}
结果:
▿ JSONToModelSwift.GoldenRetriever #0
▿ super: JSONToModelSwift.Dog
▿ name: Optional("kitty")
- some: "kitty"
▿ age: Optional(2.5)
- some: 2.5
模型 转 JSON
核心代码:
JSONEncoder().encode('遵循 Encodable 的对象')
当我们某个遵循 Codable 协议的对象想要转为 JOSN 数据时,我们则可以借助 JSONEncoder 编码器来实现。
let p = Person()
p.name = "LOLITA0164"
p.age = 26
p.address = "fuzhou"
if let jsonData = try? JSONEncoder().encode(p) {
// 编码成功,将 jsonData 转为字符输出查看
if let jsonString = String.init(data: jsonData, encoding: String.Encoding.utf8) {
print("jsonString:" + "\(jsonString)")
}
}
输出结果:
jsonString:{"name":"LOLITA0164","age":26,"address":"fuzhou"}
JSON 转 复杂数据模型
实际上,除了简单的数据模型,Codable
协议是能够完成嵌套数据模型的转换的。需要注意的是,嵌套的数据模型以及嵌套的子模型都必须遵循 Codable
协议。下面举个例子来说明。
假如我们有一个关于部门的数据模型,部门中有成员若干,可拥有管理者一名,其中的每一个人可能养了一只宠物狗。数据模型组成如下:
/// Department模型,也遵循 Codable 协议
class Department: Codable {
var name: String
var id: Int
var members: [Person] = []
var manager: Person?
}
/// Persion模型,遵循 Codable 协议
class Person: Codable {
var name: String?
var age: Int?
var address: String?
var aDog:Dog?
private enum CodingKeys: String, CodingKey {
case name = "NAME"
case age = "AGE"
case address = "ADDRESS"
case aDog = "dog"
}
}
/// Dog模型
class Dog: Codable {
var name: String?
}
解析复杂数据模型
let jsonString =
"""
{
"name":"技术部",
"id":123,
"members":[
{
"NAME":"xiaoming",
"AGE":24,
"ADDRESS":"nanjing",
"dog":{
"name":"Tom"
}
},
{
"NAME":"LOLITA0164",
"AGE":26,
"ADDRESS":"nanjing",
"dog":{
"name":"Tonny"
}
},
],
"manager":{
"NAME":"ZHANG",
"AGE":33,
"ADDRESS":"nanjing",
}
}
"""
if let jsonData = jsonString.data(using: String.Encoding.utf8) {
if let group = try? JSONDecoder().decode(Department.self, from: jsonData) {
dump(group)
}
}
结果:
▿ JSONToModelSwift.Department #0
▿ name: Optional("技术部")
- some: "技术部"
▿ id: Optional(123)
- some: 123
▿ members: 2 elements
▿ JSONToModelSwift.Person #1
▿ name: Optional("xiaoming")
- some: "xiaoming"
▿ age: Optional(24)
- some: 24
▿ address: Optional("nanjing")
- some: "nanjing"
▿ aDog: Optional(JSONToModelSwift.Dog)
▿ some: JSONToModelSwift.Dog #2
▿ name: Optional("Tom")
- some: "Tom"
▿ JSONToModelSwift.Person #3
▿ name: Optional("LOLITA0164")
- some: "LOLITA0164"
▿ age: Optional(26)
- some: 26
▿ address: Optional("nanjing")
- some: "nanjing"
▿ aDog: Optional(JSONToModelSwift.Dog)
▿ some: JSONToModelSwift.Dog #4
▿ name: Optional("Tonny")
- some: "Tonny"
▿ manager: Optional(JSONToModelSwift.Person)
▿ some: JSONToModelSwift.Person #5
▿ name: Optional("ZHANG")
- some: "ZHANG"
▿ age: Optional(33)
- some: 33
▿ address: Optional("nanjing")
- some: "nanjing"
- aDog: nil
我们可以看到,从使用上,无论解析简单的数据模型还是复杂的嵌套模型,在 JSON 转 Model 的使用方面都是一样的,实际上,Model 转 JSON 也是一致的,大家可以尝试一下。
问题和改进
虽然自定义 CodingKey
可以完成数据源和数据模型不一致的问题(这和 OC 下的一些数据模型转换采用的方式非常相似),但是在实际情况下,我们经常遇到:数据模型相同,数据来源却可能不一致,这导致一套 CodingKey
无法完成多种不同的编码和解码。那么一定要提前完成映射吗?能否在拿到数据之后,进行一次加工,将数据源处理成完全符合我们数据模型的标准再进行数据转换呢?答案是肯定的。
在 OC 的数据模型转换中,笔者通过 runtime 和 KVC 方式给数据模型赋值,以达到数据转模型的目的,其中,映射字典是其中关键的一环,目的就是通过映射字典将数据处理成标准的可直接 KVC 赋值的数据,以此将数据转模型变得更灵活。
我们先看下使用过程:
字典 转 简单数据模型
首先依旧是 Person 类 和其数据源
/// Persion模型,遵循 Codable 协议
class Person: Codable {
var name: String?
var age: Int?
var address: String?
}
// 数据字典
let dic_p:[String:Any] = [
"Name":"LOLITA0164",
"Age":26,
"address":"fuzhou",
]
使用:
// 映射字典
// '模型字段':'数据源字段'
let dic_hint = [
"name":"Name",
"age":"Age"
]
// 转换
if let p = try? LLModelTool.decode(Person.self, resDic: dic_p, hintDic: dic_hint) {
dump(p)
}
结果:
▿ JSONToModelSwift.Person #0
▿ name: Optional("LOLITA0164")
- some: "LOLITA0164"
▿ age: Optional(26)
- some: 26
▿ address: Optional("fuzhou")
- some: "fuzhou"
字典 转 嵌套数据模型
依旧是上面的例子:假如我们有一个关于部门的数据模型,部门中有成员若干,可拥有管理者一名,其中的每一个人可能有养一只宠物狗。
/// Department模型,也遵循 Codable 协议
class Department: Codable {
var name: String?
var id: Int?
var members: [Person] = []
var manager: Person?
}
/// Persion模型,遵循 Codable 协议
class Person: Codable {
var name: String?
var age: Int?
var address: String?
var aDog:Dog?
}
/// Dog模型
class Dog: Codable {
var name: String?
}
// 数据源
let dic_group: [String:Any] = [
"NAME":"技术部",
"ID":123,
"MEMBERS":[
[
"Name":"小熊",
"Age":25,
"Address":"南京",
"Dog":[
"NameString":"kitty"
],
],
[
"Name":"LOLITA0164",
"Age":26,
"Address":"fuzhou"
]
],
"Manager":[
"name":"管理者",
"age":33
]
]
使用:
// 映射字典
// '模型字段':'数据源字段'
let dic_hint2: [String:Any] = [
// Department数据模型的映射关系
"name":"NAME",
"id":"ID",
"members":"MEMBERS",
// 嵌套模型的映射关系(key 对应数据源中的 key)
"MEMBERS":[
// Person数据模型的映射关系
"name":"Name",
"age":"Age",
"address":"Address",
"aDog":"Dog",
// 嵌套模型的映射关系(key 对应数据源中的 key)
"Dog":[
// Dog数据模型的映射关系
"name":"NameString"
]
],
"manager":"Manager"
]
if let group = try? LLModelTool.decode(Department.self, resDic: dic_group, hintDic: dic_hint2) {
dump(group)
}
结果:
▿ JSONToModelSwift.Department #0
▿ name: Optional("技术部")
- some: "技术部"
▿ id: Optional(123)
- some: 123
▿ members: 2 elements
▿ JSONToModelSwift.Person #1
▿ name: Optional("小熊")
- some: "小熊"
▿ age: Optional(25)
- some: 25
▿ address: Optional("南京")
- some: "南京"
▿ aDog: Optional(JSONToModelSwift.Dog)
▿ some: JSONToModelSwift.Dog #2
▿ name: Optional("kitty")
- some: "kitty"
▿ JSONToModelSwift.Person #3
▿ name: Optional("LOLITA0164")
- some: "LOLITA0164"
▿ age: Optional(26)
- some: 26
▿ address: Optional("fuzhou")
- some: "fuzhou"
- aDog: nil
▿ manager: Optional(JSONToModelSwift.Person)
▿ some: JSONToModelSwift.Person #4
▿ name: Optional("管理者")
- some: "管理者"
▿ age: Optional(33)
- some: 33
- address: nil
- aDog: nil
注:如果只有少数部分是不统一的,我们也可以通过 CodingKey
将部分统一的字段编写对应关系,少数部分通过映射字典更换资源字典数据,以完成转换。
例如:
/// Persion模型,遵循 Codable 协议
class Person: Codable {
var name: String?
var age: Int?
var address: String?
// CodingKeys 只有两个映射枚举
private enum CodingKeys: String, CodingKey {
case name = "NAME"
case age = "AGE"
case address
}
}
// 源字典中有第三个字段和 CodingKeys 中的不一致
let dic_p:[String:Any] = [
"NAME":"LOLITA0164",
"AGE":26,
"ADDRESS":"fuzhou",
]
// 映射字典,只需映射不一致的即可
let dic_hint = [
"address":"ADDRESS",
]
if let p = try? LLModelTool.decode(Person.self, resDic: dic_p, hintDic: dic_hint) {
dump(p)
}
实现过程
首先,我们将 JSONDecoder().decode()
进行再次封装:
/// 字典 转 模型
static func decode<T>(_ type: T.Type, resDic: [String:Any] , hintDic:[String:Any]?) throws -> T where T: Decodable {
var transformDic = resDic
if (hintDic != nil) {
// 将映射字典转换成模型所需的字典
transformDic = self.setUpResourceDic(resDic: resDic, hintDic: hintDic!)
}
guard let jsonData = self.getJsonData(param: transformDic) else {
throw LLModelToolError.message("转成 Data 时出错!!!")
}
guard let model = try? JSONDecoder().decode(type, from: jsonData)
else {
throw LLModelToolError.message("转成 数据模型 时出错!!!")
}
return model
}
我们可以看到,该方法的核心依旧是系统的转换方法,我们要做的就是将映射字典转换成模型所需的字典,然后的处理一切照旧。
核心的转换方法如下:
/// 根据映射字典设置当前字典内容
private static func setUpResourceDic(resDic: [String:Any] , hintDic:[String:Any]) -> [String:Any]{
var transformDic = resDic
for (key,value) in hintDic {
let valueNew: AnyObject = value as AnyObject
if valueNew.classForCoder == NSDictionary.classForCoder(){ // 模型映射
let res_value = resDic[key] as AnyObject // 为了获取数据类型
if res_value.classForCoder == NSArray.classForCoder(){ // 数据类型为数组(模型数组)
let res_value_array = res_value as! [[String:Any]]
var resArray: [Any] = []
for item in res_value_array {
// 递归调用,寻找子模型
let res = self.setUpResourceDic(resDic: item , hintDic: valueNew as! [String : Any])
resArray.append(res)
}
let realKey = self.getRealKey(key: key, dic: hintDic)
transformDic[realKey] = resArray
// 移除旧的数据
if realKey != key {
transformDic.removeValue(forKey: key)
}
}
else if res_value.classForCoder == NSDictionary.classForCoder(){ // 数据类型为字典(模型)
// 递归调用,寻找子模型
let res = self.setUpResourceDic(resDic: res_value as! [String : Any] , hintDic: valueNew as! [String : Any])
let realKey = self.getRealKey(key: key, dic: hintDic)
transformDic[realKey] = res
// 移除旧的数据
if realKey != key {
transformDic.removeValue(forKey: key)
}
}
}else if valueNew.classForCoder == NSString.classForCoder(){ // 普通映射
// 去掉
if !hintDic.keys.contains(valueNew as! String){
transformDic[key] = resDic[valueNew as! String]
}
// 移除旧的数据
if key != valueNew as! String {
transformDic.removeValue(forKey: valueNew as! String)
}
}
}
return transformDic
}
转换的思路为:
1、中心思想无非就是进行 key 的替换
2、遍历映射字典,如果映射字典中是 "String":"String"
我们直接进行替换(先新增数据,再将就数据删除),如果是 "String":"Dictionary"
,则表示该字段中的 Dictionary 是一个数据模型,此时我们需要取出该字典,采用递归的方式深层次的寻找和替换。
缺点建议
复杂的数据模型在使用起来不是非常的顺手,因为我们需要为其集中编写复杂的对应关系,因此不如将数据拆成简单的数据模型,再赋值给复杂模型,这样映射字典变得简单很多,也更易阅读。
完整的代码为:
import Foundation
enum LLModelToolError: Error {
case message(String)
}
struct LLModelTool {
/// 字典 转 模型
static func decode<T>(_ type: T.Type, resDic: [String:Any] , hintDic:[String:Any]?) throws -> T where T: Decodable {
// 将映射字典转换成模型所需的字典
var transformDic = resDic
if (hintDic != nil) {
transformDic = self.setUpResourceDic(resDic: resDic, hintDic: hintDic!)
}
guard let jsonData = self.getJsonData(param: transformDic) else {
throw LLModelToolError.message("转成 Data 时出错!!!")
}
guard let model = try? JSONDecoder().decode(type, from: jsonData)
else {
throw LLModelToolError.message("转成 数据模型 时出错!!!")
}
return model
}
/// json 转模型
static func decode<T>(_ type: T.Type, jsonData: Data , hintDic:[String:Any]?) throws -> T where T: Decodable {
guard let resDic: [String:Any] = try? JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.mutableContainers) as! [String : Any] else {
throw LLModelToolError.message("转成 字典 时出错!!!")
}
return try! self.decode(type, resDic: resDic, hintDic: hintDic)
}
// 模型转字典
static func reflectToDict<T>(model: T) -> [String:Any] {
let mirro = Mirror(reflecting: model)
var dict = [String:Any]()
for case let (key?, value) in mirro.children {
dict[key] = value
}
return dict
}
/// 获取 json 数据,data类型
static func getJsonData(param: Any) -> Data? {
if !JSONSerialization.isValidJSONObject(param) {
return nil
}
guard let data = try? JSONSerialization.data(withJSONObject: param, options: []) else {
return nil
}
return data
}
/// 根据映射字典设置当前字典内容
private static func setUpResourceDic(resDic: [String:Any] , hintDic:[String:Any]) -> [String:Any]{
var transformDic = resDic
for (key,value) in hintDic {
let valueNew: AnyObject = value as AnyObject
if valueNew.classForCoder == NSDictionary.classForCoder(){ // 模型映射
let res_value = resDic[key] as AnyObject // 为了获取数据类型
if res_value.classForCoder == NSArray.classForCoder(){ // 数据类型为数组(模型数组)
let res_value_array = res_value as! [[String:Any]]
var resArray: [Any] = []
for item in res_value_array {
// 递归调用,寻找子模型
let res = self.setUpResourceDic(resDic: item , hintDic: valueNew as! [String : Any])
resArray.append(res)
}
let realKey = self.getRealKey(key: key, dic: hintDic)
transformDic[realKey] = resArray
// 移除旧的数据
if realKey != key {
transformDic.removeValue(forKey: key)
}
}
else if res_value.classForCoder == NSDictionary.classForCoder(){ // 数据类型为字典(模型)
// 递归调用,寻找子模型
let res = self.setUpResourceDic(resDic: res_value as! [String : Any] , hintDic: valueNew as! [String : Any])
let realKey = self.getRealKey(key: key, dic: hintDic)
transformDic[realKey] = res
// 移除旧的数据
if realKey != key {
transformDic.removeValue(forKey: key)
}
}
}else if valueNew.classForCoder == NSString.classForCoder(){ // 普通映射
// 去掉
if !hintDic.keys.contains(valueNew as! String){
transformDic[key] = resDic[valueNew as! String]
}
// 移除旧的数据
if key != valueNew as! String {
transformDic.removeValue(forKey: valueNew as! String)
}
}
}
return transformDic
}
/// 从映射字典中获取到模型中对应的key
private static func getRealKey(key:String, dic:[String:Any]) -> String {
for (k,v) in dic {
let value: AnyObject = v as AnyObject
if value.classForCoder == NSString.classForCoder(){
let valueNew = value as! String
if valueNew == key{
return k
}
}
}
return key
}
}
补充说明
在数据模型中的成员变量中,基本数据类型如:String
、Int
、Float
等都已经实现了 Codable 协议,因此如果你的数据类型只包含这些基本数据类型的属性,只需要在类型声明中加上 Codable 协议就可以了,不需要写任何实际实现的代码。
但是,一些特殊类型还有有一些限制
- 枚举
枚举需要声明原始值的类型,并且声明遵循 Codable
协议。
enum Sex: String ,Codable {
case female
case male
}
// 数据模型
class People: Codable {
var sex: Sex?
}
// 数据源
let dic: [String : Any] = [
"sex":"male",
]
// 转换
if let p = try? LLModelTool.decode(People.self, resDic: dic, hintDic: nil) {
dump(p)
}
输出:
▿ JSONToModelSwift.People #0
▿ sex: Optional(JSONToModelSwift.Sex.male)
- some: JSONToModelSwift.Sex.male
- 布尔型
Bool 类型默认只支持 true/false 形式的 Bool 值解析。对于一些使用 0/1 形式来表示 Bool 值的后端框架,只能通过 Int 类型解析之后再做转换了,或者可以自定义实现 Codable 协议。
enum Sex: String ,Codable {
case female
case male
}
// 数据模型
class People: Codable {
var sex: Sex?
var isTall: Bool? = nil
}
// 数据源
let dic: [String : Any] = [
"sex":"male",
"isTall":true
]
// 转换
if let p = try? LLModelTool.decode(People.self, resDic: dic, hintDic: nil) {
dump(p)
}
输出:
▿ JSONToModelSwift.People #0
▿ sex: Optional(JSONToModelSwift.Sex.male)
- some: JSONToModelSwift.Sex.male
▿ isTall: Optional(true)
- some: true
参考
三方转换库
这些库我都没有使用过,仅仅是从其他人那边摘抄过来做备份,读者有兴趣可以试一试。