簡單說明
在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
參考
三方轉換庫
這些庫我都沒有使用過,僅僅是從其他人那邊摘抄過來做備份,讀者有興趣可以試一試。