本篇是Swift4中Codable的使用系列第二篇,繼上一篇文章,我們學習了
Codable
協議在json與模型之間編碼和解碼的基本使用。本篇我們將了解Codable中,如何實現自定義模型轉json編碼和自定義json轉模型解碼的過程。
對于自定義模型轉json編碼和自定義json轉模型解碼的過程,我們只需要在該類型中重寫Codable
協議中的編碼和解碼方法即可:
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
public protocol Decodable {
public init(from decoder: Decoder) throws
}
public typealias Codable = Decodable & Encodable
我們先定義一個Student模型來進行演示:
struct Student: Codable {
let name: String
let age: Int
let bornIn: String
// 映射規則,用來指定屬性和json中key兩者間的映射的規則
enum CodingKeys: String, CodingKey {
case name
case age
case bornIn = "born_in"
}
}
重寫系統的方法,實現與系統一樣的decode和encode效果
在自定義前,我們先來把這兩個方法重寫成系統默認的實現來了解一下,對于這兩個方法,我們要掌握的是container
的用法。
init(name: String, age: Int, bornIn: String) {
self.name = name
self.age = age
self.bornIn = bornIn
}
// 重寫decoding
init(from decoder: Decoder) throws {
// 通過指定映射規則來創建解碼容器,通過該容器獲取json中的數據,因此是個常量
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let age = try container.decode(Int.self, forKey: .age)
let bornIn = try container.decode(String.self, forKey: .bornIn)
self.init(name: name, age: age, bornIn: bornIn)
}
// 重寫encoding
func encode(to encoder: Encoder) throws {
// 通過指定映射規則來創建編碼碼容器,通過往容器里添加內容最后生成json,因此是個變量
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
try container.encode(bornIn, forKey: .bornIn)
}
對于編碼和解碼的過程,我們都是創建一個容器,該容器有一個keyedBy
的參數,用于指定屬性和json中key兩者間的映射的規則,因此這次我們傳CodingKeys
的類型過去,說明我們要使用該規則來映射。對于解碼的過程,我們使用該容器來進行解碼,指定要值的類型和獲取哪一個key的值,同樣的,編碼的過程中,我們使用該容器來指定要編碼的值和該值對應json中的key,他們看起來有點像Dictionary
的用法。還是使用上一篇的泛型函數來進行encode和decode:
func encode<T>(of model: T) throws where T: Codable {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encodedData = try encoder.encode(model)
print(String(data: encodedData, encoding: .utf8)!)
}
func decode<T>(of jsonString: String, type: T.Type) throws -> T where T: Codable {
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let model = try decoder.decode(T.self, from: data)
return model
}
現在我們來驗證我們重寫寫的是否正確:
let res = """
{
"name": "Jone",
"age": 17,
"born_in": "China"
}
"""
let stu = try! decode(of: res, type: Student.self)
dump(stu)
try! encode(of: stu)
//? __lldb_expr_1.Student
// - name: "Jone"
// - age: 17
// - bornIn: "China"
//{
// "name" : "Jone",
// "age" : 17,
// "born_in" : "China"
//}
打印的結果是正確的,現在我們重寫的方法實現了和原生的一樣效果。
使用struct來遵守CodingKey來指定映射規則
接著我們倒回去看我們定義的模型,模型中定義的CodingKeys
映射規則是用enum
來遵守CodingKey
協議實現的,其實我們還可以把CodingKeys
的類型定義一個struct
來實現CodingKey
協議:
// 映射規則,用來指定屬性和json中key兩者間的映射的規則
// enum CodingKeys: String, CodingKey {
// case name
// case age
// case bornIn = "born_in"
// }
// 映射規則,用來指定屬性和json中key兩者間的映射的規則
struct CodingKeys: CodingKey {
var stringValue: String //key
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
// 在decode過程中,這里傳入的stringValue就是json中對應的key,然后獲取該key的值
// 在encode過程中,這里傳入的stringValue就是生成的json中對應的key,然后設置key的值
init?(stringValue: String) {
self.stringValue = stringValue
}
// 相當于enum中的case
static let name = CodingKeys(stringValue: "name")!
static let age = CodingKeys(stringValue: "age")!
static let bornIn = CodingKeys(stringValue: "born_in")!
}
使用結構體來遵守該協議需要實現該協議的內容,這里因為我們的json中的key是String
類型,所以用不到intValue
,因此返回nil即可。重新運行,結果仍然是正確的。不過需要注意的是,如果 不是 使用enum
來遵守CodingKey
協議的話,例如用struct
,我們 必須 重寫Codable
協議里的編碼和解碼方法,否者就會報錯:
cannot automatically synthesize 'Decodable' because 'CodingKeys' is not an enum
cannot automatically synthesize 'Encodable' because 'CodingKeys' is not an enum
因此,使用struct
來遵守CodingKey
,比用enum
工程量大。那為什么還要提出這種用法?因為在某些特定的情況下它還是有出場的機會,使用struct
來指定映射規則更靈活,到在第三篇中的一個例子就會講到使用的場景,這里先明白它的工作方式。
自定義Encoding
在自定義encode中,我們需要注意的點是對時間格式處理,Optional值處理以及數組處理。
時間格式處理
上一篇文章也提及過關于對時間格式的處理,這里我們有兩個方法對時間格式進行自定義encode。
方法一:在encode方法中處理
struct Student: Codable {
let registerTime: Date
enum CodingKeys: String, CodingKey {
case registerTime = "register_time"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy HH:mm:ssZ"
let stringDate = formatter.string(from: registerTime)
try container.encode(stringDate, forKey: .registerTime)
}
}
方法二: 對泛型函數中對JSONEncoder對象的dateEncodingStrategy屬性進行設置
encoder.dateEncodingStrategy = .custom { (date, encoder) in
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy HH:mm:ssZ"
let stringDate = formatter.string(from: date)
var container = encoder.singleValueContainer()
try container.encode(stringDate)
}
這里創建的容器是一個singleValueContainer
,因為這里不像encode方法中那樣需要往容器里一直添加值,所以使用一個單值容器就可以了。
try! encode(of: Student(registerTime: Date()))
//{
// "register_time" : "Nov-13-2017 20:12:57+0800"
//}
Optional值處理
如果模型中有屬性是可選值,并且為nil,當我進行encode時該值是不會以null的形式寫入json中:
struct Student: Codable {
var scores: [Int]?
}
try! encode(of: Student())
//{
//
//}
因為系統對encode的實現其實不是像我們上面所以寫的那樣用container調用encode
方法,而是調用encodeIfPresent
這個方法,該方法對nil則不進行encode。我們可以強制將friends寫入json中:
struct Student: Codable {
var scores: [Int]?
enum CodingKeys: String, CodingKey {
case scores
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(scores, forKey: .scores)
}
}
try! encode(of: Student())
//{
// "scores" : null
//}
數組處理
有時候,我們想對一個數組類型的屬性進行處理后再進行encode,或許你會想,使用一個compute property處理就可以了,但是你只是想將處理后的數組進行encode,原來的數組則不需要,于是你自定義encode來實現,然后!你突然就不想多寫一個compute property,只想在encode方法里進行處理,于是我們可以使用container的nestedUnkeyedContainer(forKey:)
方法創建一個UnkeyedEncdingContainer
(顧名思義,數組是沒有key的)來對于數組進行處理就可以了。
struct Student: Codable {
let scores: [Int] = [66, 77, 88]
enum CodingKeys: String, CodingKey {
case scores
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// 創建一個對數組處理用的容器 (UnkeyedEncdingContainer)
var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .scores)
try scores.forEach {
try unkeyedContainer.encode("\($0)分")
}
}
}
try! encode(of: Student())
//{
// "scores" : [
// "66分",
// "77分",
// "88分"
// ]
//}
自定義Decoding
對于自定義decode操作上與自定義encode類似,需要說明的點同樣也是時間格式處理,數組處理,但Optional值就不用理會了。
時間格式處理
當我們嘗試寫出一下自定義decode代碼時就會拋出一個錯誤:
struct Student: Codable {
let registerTime: Date
enum CodingKeys: String, CodingKey {
case registerTime = "register_time"
}
init(registerTime: Date) {
self.registerTime = registerTime
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let registerTime = try container.decode(Date.self, forKey: .registerTime)
self.init(registerTime: registerTime)
}
}
let res = """
{
"register_time": "2017-11-13 22:30:15 +0800"
}
"""
let stu = try! decode(of: res, type: Student.self) ?
// error: Expected to decode Double but found a string/data instead.
因為我們這里時間的格式不是一個浮點數,而是有一定格式化的字符串,因此我們要進行對應的格式匹配,操作也是和自定義encode中的類似,修改init(from decoder: Decoder
方法:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateString = try container.decode(Date.self, forKey: .registerTime)
let formaater = DateFormatter()
formaater.dateFormat = "yyyy-MM-dd HH:mm:ss z"
let registerTime = formaater.date(from: dateString)!
self.init(registerTime: registerTime)
}
或者我們可以在JSONDecoder
對象對dateDncodingStrategy
屬性使用custom來修改:
decoder.dateDecodingStrategy = .custom{ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
return formatter.date(from: dateString)!
}
數組處理
當我們獲取這樣的數據:
let res = """
{
"gross_score": 120,
"scores": [
0.65,
0.75,
0.85
]
}
"""
gross_score代表該科目的總分數,scores里裝的是分數占總分數的比例,我們需要將它們轉換成實際的分數再進行初始化。對于數組的處理,我們和自定義encoding時所用的容器都是UnkeyedContainer,通過container的nestedUnkeyedContainer(forKey: )
方法創建一個UnkeyedDecodingContainer
,然后從這個unkeyedContainer中不斷取出值來decode,并指定其類型。
struct Student: Codable {
let grossScore: Int
let scores: [Float]
enum CodingKeys: String, CodingKey {
case grossScore = "gross_score"
case scores
}
init(grossScore: Int, scores: [Float]) {
self.grossScore = grossScore
self.scores = scores
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let grossScore = try container.decode(Int.self, forKey: .grossScore)
var scores = [Float]()
// 處理數組時所使用的容器(UnkeyedDecodingContainer)
var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .scores)
// isAtEnd:A Boolean value indicating whether there are no more elements left to be decoded in the container.
while !unkeyedContainer.isAtEnd {
let proportion = try unkeyedContainer.decode(Float.self)
let score = proportion * Float(grossScore)
scores.append(score)
}
self.init(grossScore: grossScore, scores: scores)
}
}
扁平化JSON的編碼和解碼
現在我們已經熟悉了自定義encoding和decoding的過程了,也知道對數組處理要是container創建的nestedUnkeyedContainer(forKey: )
創建的unkeyedContainer來處理。現在我們來看一個場景,假設我們有這樣一組含嵌套結構的數據:
let res = """
{
"name": "Jone",
"age": 17,
"born_in": "China",
"meta": {
"gross_score": 120,
"scores": [
0.65,
0.75,
0.85
]
}
}
"""
而我們定義的模型的結構卻是扁平的:
struct Student {
let name: String
let age: Int
let bornIn: String
let grossScore: Int
let scores: [Float]
}
對于這類場景,我們可以使用container的nestedContainer(keyedBy:, forKey: )
方法創建的KeyedContainer處理,同樣是處理內嵌類型的容器,既然有處理像數組這樣unkey的內嵌類型的容器,自然也有處理像字典這樣有key的內嵌類型的容器,在encoding中是KeyedEncodingContainer
類型,而在decoding中當然是KeyedDecodingContainer
類型,因為encoding和decoding中它們是相似的:
struct Student: Codable {
let name: String
let age: Int
let bornIn: String
let grossScore: Int
let scores: [Float]
enum CodingKeys: String, CodingKey {
case name
case age
case bornIn = "born_in"
case meta
}
// 這里要指定嵌套的數據中的映射規則
enum MetaCodingKeys: String, CodingKey {
case grossScore = "gross_score"
case scores
}
init(name: String, age: Int, bornIn: String, grossScore: Int, scores: [Float]) {
self.name = name
self.age = age
self.bornIn = bornIn
self.grossScore = grossScore
self.scores = scores
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let age = try container.decode(Int.self, forKey: .age)
let bornIn = try container.decode(String.self, forKey: .bornIn)
// 創建一個對字典處理用的容器 (KeyedDecodingContainer),并指定json中key和屬性名的規則
let keyedContainer = try container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
let grossScore = try keyedContainer.decode(Int.self, forKey: .grossScore)
var unkeyedContainer = try keyedContainer.nestedUnkeyedContainer(forKey: .scores)
var scores = [Float]()
while !unkeyedContainer.isAtEnd {
let proportion = try unkeyedContainer.decode(Float.self)
let score = proportion * Float(grossScore)
scores.append(score)
}
self.init(name: name, age: age, bornIn: bornIn, grossScore: grossScore, scores: scores)
}
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(bornIn, forKey: .bornIn)
// 創建一個對字典處理用的容器 (KeyedEncodingContainer),并指定json中key和屬性名的規則
var keyedContainer = container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
try keyedContainer.encode(grossScore, forKey: .grossScore)
var unkeyedContainer = keyedContainer.nestedUnkeyedContainer(forKey: .scores)
try scores.forEach {
try unkeyedContainer.encode("\($0)分")
}
}
}
然后我們驗證一下:
let stu = try! decode(of: res, type: Student.self)
dump(stu)
try! encode(of: stu)
//? __lldb_expr_82.Student
// - name: "Jone"
// - age: 17
// - bornIn: "China"
// - grossScore: 120
// ? scores: 3 elements
// - 78.0
// - 90.0
// - 102.0
//
//{
// "age" : 17,
// "meta" : {
// "gross_score" : 120,
// "scores" : [
// "78.0分",
// "90.0分",
// "102.0分"
// ]
// },
// "born_in" : "China",
// "name" : "Jone"
//}
現在我們實現了嵌套結構的json和扁平模型之間的轉換了。
至此我們學會了如何自定義encoding和decoding,其中的關鍵在與掌握
container
的使用,根據不同情況使用不同的container,實際情況千差萬別,可是套路總是類似,我們見招拆招就好了。