本篇是Swift4中Codable的使用系列第二篇,繼上一篇文章,我們學(xué)習(xí)了
Codable協(xié)議在json與模型之間編碼和解碼的基本使用。本篇我們將了解Codable中,如何實(shí)現(xiàn)自定義模型轉(zhuǎn)json編碼和自定義json轉(zhuǎn)模型解碼的過程。
對于自定義模型轉(zhuǎn)json編碼和自定義json轉(zhuǎn)模型解碼的過程,我們只需要在該類型中重寫Codable協(xié)議中的編碼和解碼方法即可:
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模型來進(jìn)行演示:
struct Student: Codable {
let name: String
let age: Int
let bornIn: String
// 映射規(guī)則,用來指定屬性和json中key兩者間的映射的規(guī)則
enum CodingKeys: String, CodingKey {
case name
case age
case bornIn = "born_in"
}
}
重寫系統(tǒng)的方法,實(shí)現(xiàn)與系統(tǒng)一樣的decode和encode效果
在自定義前,我們先來把這兩個方法重寫成系統(tǒng)默認(rèn)的實(shí)現(xiàn)來了解一下,對于這兩個方法,我們要掌握的是container的用法。
init(name: String, age: Int, bornIn: String) {
self.name = name
self.age = age
self.bornIn = bornIn
}
// 重寫decoding
init(from decoder: Decoder) throws {
// 通過指定映射規(guī)則來創(chuàng)建解碼容器,通過該容器獲取json中的數(shù)據(jù),因此是個常量
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 {
// 通過指定映射規(guī)則來創(chuàng)建編碼碼容器,通過往容器里添加內(nèi)容最后生成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)
}
對于編碼和解碼的過程,我們都是創(chuàng)建一個容器,該容器有一個keyedBy的參數(shù),用于指定屬性和json中key兩者間的映射的規(guī)則,因此這次我們傳CodingKeys的類型過去,說明我們要使用該規(guī)則來映射。對于解碼的過程,我們使用該容器來進(jìn)行解碼,指定要值的類型和獲取哪一個key的值,同樣的,編碼的過程中,我們使用該容器來指定要編碼的值和該值對應(yīng)json中的key,他們看起來有點(diǎn)像Dictionary的用法。還是使用上一篇的泛型函數(shù)來進(jìn)行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
}
現(xiàn)在我們來驗(yàn)證我們重寫寫的是否正確:
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"
//}
打印的結(jié)果是正確的,現(xiàn)在我們重寫的方法實(shí)現(xiàn)了和原生的一樣效果。
使用struct來遵守CodingKey來指定映射規(guī)則
接著我們倒回去看我們定義的模型,模型中定義的CodingKeys映射規(guī)則是用enum來遵守CodingKey協(xié)議實(shí)現(xiàn)的,其實(shí)我們還可以把CodingKeys的類型定義一個struct來實(shí)現(xiàn)CodingKey協(xié)議:
// 映射規(guī)則,用來指定屬性和json中key兩者間的映射的規(guī)則
// enum CodingKeys: String, CodingKey {
// case name
// case age
// case bornIn = "born_in"
// }
// 映射規(guī)則,用來指定屬性和json中key兩者間的映射的規(guī)則
struct CodingKeys: CodingKey {
var stringValue: String //key
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
// 在decode過程中,這里傳入的stringValue就是json中對應(yīng)的key,然后獲取該key的值
// 在encode過程中,這里傳入的stringValue就是生成的json中對應(yīng)的key,然后設(shè)置key的值
init?(stringValue: String) {
self.stringValue = stringValue
}
// 相當(dāng)于enum中的case
static let name = CodingKeys(stringValue: "name")!
static let age = CodingKeys(stringValue: "age")!
static let bornIn = CodingKeys(stringValue: "born_in")!
}
使用結(jié)構(gòu)體來遵守該協(xié)議需要實(shí)現(xiàn)該協(xié)議的內(nèi)容,這里因?yàn)槲覀兊膉son中的key是String類型,所以用不到intValue,因此返回nil即可。重新運(yùn)行,結(jié)果仍然是正確的。不過需要注意的是,如果 不是 使用enum來遵守CodingKey協(xié)議的話,例如用struct,我們 必須 重寫Codable協(xié)議里的編碼和解碼方法,否者就會報錯:
cannot automatically synthesize 'Decodable' because 'CodingKeys' is not an enum
cannot automatically synthesize 'Encodable' because 'CodingKeys' is not an enum
因此,使用struct來遵守CodingKey,比用enum工程量大。那為什么還要提出這種用法?因?yàn)樵谀承┨囟ǖ那闆r下它還是有出場的機(jī)會,使用struct來指定映射規(guī)則更靈活,到在第三篇中的一個例子就會講到使用的場景,這里先明白它的工作方式。
自定義Encoding
在自定義encode中,我們需要注意的點(diǎn)是對時間格式處理,Optional值處理以及數(shù)組處理。
時間格式處理
上一篇文章也提及過關(guān)于對時間格式的處理,這里我們有兩個方法對時間格式進(jìn)行自定義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)
}
}
方法二: 對泛型函數(shù)中對JSONEncoder對象的dateEncodingStrategy屬性進(jìn)行設(shè)置
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)
}
這里創(chuàng)建的容器是一個singleValueContainer,因?yàn)檫@里不像encode方法中那樣需要往容器里一直添加值,所以使用一個單值容器就可以了。
try! encode(of: Student(registerTime: Date()))
//{
// "register_time" : "Nov-13-2017 20:12:57+0800"
//}
Optional值處理
如果模型中有屬性是可選值,并且為nil,當(dāng)我進(jìn)行encode時該值是不會以null的形式寫入json中:
struct Student: Codable {
var scores: [Int]?
}
try! encode(of: Student())
//{
//
//}
因?yàn)橄到y(tǒng)對encode的實(shí)現(xiàn)其實(shí)不是像我們上面所以寫的那樣用container調(diào)用encode方法,而是調(diào)用encodeIfPresent這個方法,該方法對nil則不進(jìn)行encode。我們可以強(qiáng)制將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
//}
數(shù)組處理
有時候,我們想對一個數(shù)組類型的屬性進(jìn)行處理后再進(jìn)行encode,或許你會想,使用一個compute property處理就可以了,但是你只是想將處理后的數(shù)組進(jìn)行encode,原來的數(shù)組則不需要,于是你自定義encode來實(shí)現(xiàn),然后!你突然就不想多寫一個compute property,只想在encode方法里進(jìn)行處理,于是我們可以使用container的nestedUnkeyedContainer(forKey:)方法創(chuàng)建一個UnkeyedEncdingContainer(顧名思義,數(shù)組是沒有key的)來對于數(shù)組進(jìn)行處理就可以了。
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)
// 創(chuàng)建一個對數(shù)組處理用的容器 (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類似,需要說明的點(diǎn)同樣也是時間格式處理,數(shù)組處理,但Optional值就不用理會了。
時間格式處理
當(dāng)我們嘗試寫出一下自定義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.
因?yàn)槲覀冞@里時間的格式不是一個浮點(diǎn)數(shù),而是有一定格式化的字符串,因此我們要進(jìn)行對應(yīng)的格式匹配,操作也是和自定義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)!
}
數(shù)組處理
當(dāng)我們獲取這樣的數(shù)據(jù):
let res = """
{
"gross_score": 120,
"scores": [
0.65,
0.75,
0.85
]
}
"""
gross_score代表該科目的總分?jǐn)?shù),scores里裝的是分?jǐn)?shù)占總分?jǐn)?shù)的比例,我們需要將它們轉(zhuǎn)換成實(shí)際的分?jǐn)?shù)再進(jìn)行初始化。對于數(shù)組的處理,我們和自定義encoding時所用的容器都是UnkeyedContainer,通過container的nestedUnkeyedContainer(forKey: )方法創(chuàng)建一個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]()
// 處理數(shù)組時所使用的容器(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的編碼和解碼
現(xiàn)在我們已經(jīng)熟悉了自定義encoding和decoding的過程了,也知道對數(shù)組處理要是container創(chuàng)建的nestedUnkeyedContainer(forKey: )創(chuàng)建的unkeyedContainer來處理?,F(xiàn)在我們來看一個場景,假設(shè)我們有這樣一組含嵌套結(jié)構(gòu)的數(shù)據(jù):
let res = """
{
"name": "Jone",
"age": 17,
"born_in": "China",
"meta": {
"gross_score": 120,
"scores": [
0.65,
0.75,
0.85
]
}
}
"""
而我們定義的模型的結(jié)構(gòu)卻是扁平的:
struct Student {
let name: String
let age: Int
let bornIn: String
let grossScore: Int
let scores: [Float]
}
對于這類場景,我們可以使用container的nestedContainer(keyedBy:, forKey: )方法創(chuàng)建的KeyedContainer處理,同樣是處理內(nèi)嵌類型的容器,既然有處理像數(shù)組這樣unkey的內(nèi)嵌類型的容器,自然也有處理像字典這樣有key的內(nèi)嵌類型的容器,在encoding中是KeyedEncodingContainer類型,而在decoding中當(dāng)然是KeyedDecodingContainer類型,因?yàn)閑ncoding和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
}
// 這里要指定嵌套的數(shù)據(jù)中的映射規(guī)則
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)
// 創(chuàng)建一個對字典處理用的容器 (KeyedDecodingContainer),并指定json中key和屬性名的規(guī)則
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)
// 創(chuàng)建一個對字典處理用的容器 (KeyedEncodingContainer),并指定json中key和屬性名的規(guī)則
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)分")
}
}
}
然后我們驗(yàn)證一下:
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"
//}
現(xiàn)在我們實(shí)現(xiàn)了嵌套結(jié)構(gòu)的json和扁平模型之間的轉(zhuǎn)換了。
至此我們學(xué)會了如何自定義encoding和decoding,其中的關(guān)鍵在與掌握
container的使用,根據(jù)不同情況使用不同的container,實(shí)際情況千差萬別,可是套路總是類似,我們見招拆招就好了。