原文:https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift
基礎(chǔ)語法
Swift將 encoding和 decoding能力內(nèi)置在了系統(tǒng)庫(Codable協(xié)議)中,不需要像OC一樣編寫從JSON String 到 對象的decoding方法(及其逆方法),我們所需要做的只有:
- 遵守Codable協(xié)議
struct Toy: Codable {
var name: String
}
struct Employee: Codable {
var name: String
var id: Int
var favoriteToy: Toy
}
- 聲明encode & 1. decoder
let encoder = JSONEncoder()
let decoder = JSONDecoder()
- 執(zhí)行編碼和解碼
let toy = Toy(name: "Teddy Bear")
let employee = Employee(name: "John Appleseed", id: 7, favoriteToy: toy)
let data = try encoder.encode(employee)
let string = String(data: data, encoding: .utf8)
let sameEmployee = try decoder.decode(Employee.self, from: data)
Codable協(xié)議的能力
嵌套編碼
一段用utf8編碼過得JSON下發(fā)到客戶端,實(shí)際上就相當(dāng)于上述代碼中的string,格式化處理后相當(dāng)于:
{
"name" : "John Appleseed",
"id" : 7,
"favoriteToy" : {
"name" : "Teddy Bear"
}
}
對于favoriteToy字段下嵌套的可以對應(yīng)到Toy的JSON結(jié)構(gòu),由于Toy類型也遵守了Codable協(xié)議,也會被正確轉(zhuǎn)為Toy對象
Snake Case 和 Camel Case之間的轉(zhuǎn)換
可以幫助解決,客戶端常用Camel Case 但 API可能使用Snake Case的問題:
encoder.keyEncodeingStrategy = .convertToSnakeCase
decoder.keyDecodingStratefgy = .convertFromSnakeCase
猜測內(nèi)部會對key先做一層轉(zhuǎn)換再去encode/decode
自定義JSON Key
可能出現(xiàn)客戶端和API對key的定義不一樣的情況,如API將favoriteToy改成了gift,客戶端仍想保留變量名為favoriteToy,客戶端可以選擇自定義key值:
struct Employee: Codable {
var name: String
var id: Int
var favoriteToy: Toy
enum CodingKeys: String, CodingKey {
case name, id, favoriteToy = "gift"
}
}
需要注意的是,只有被列入CodingKeys枚舉的變量才會被解析/編碼,所以即使不需要自定義,需要的變量名仍要寫進(jìn)去
處理與客戶端不同的JSON數(shù)據(jù)結(jié)構(gòu)
Flat JSON
假如API把JSON都改成Flat的了:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : "Teddy Bear"
}
和Employee數(shù)據(jù)結(jié)構(gòu)不一樣,客戶端gift是一個Toy對象而非字符串,編譯器無法自動生成,只能自己寫decode(init)和encode方法了:
struct Toy: Codable {
var name: String
}
struct Employee: Encodable {
var name: String
var id: Int
var favoriteToy: Toy
enum CodingKeys: CodingKey {
case name, id, gift
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) // 聲明一個container,可以像Dictionory用指定鍵存儲內(nèi)容
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encode(favoriteToy.name, forKey: .gift) // 關(guān)鍵:指定key為gift去encode favoriteToy的內(nèi)容
}
}
// 寫在extension里是為了保持Swift struct的member-wise initializer, 如果在struct的主定義里寫了init方法就會導(dǎo)致失效
extension Employee: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
let gift = try container.decode(String.self, forKey: .gift)
favoriteToy = Toy(name: gift)
}
}
let toy = Toy(name: "Teddy Bear")
let employee = Employee(name: "John Appleseed", id: 7, favoriteToy: toy)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(employee)
let string = String(data: data, encoding: .utf8)!
let sameEmployee = try decoder.decode(Employee.self, from: data)
Deep JSON
假如API把JSON的數(shù)據(jù)結(jié)構(gòu)又改成了這樣:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"toy" : {
"name" : "Teddy Bear"
}
}
}
又和Employee結(jié)構(gòu)不一樣,還是得像flat json的情況一樣,重寫encode和decode(init)方法,變化點(diǎn):嵌套層加一個Keys枚舉;嵌套層通過nestedContainer裝入外層
struct Employee: Encodable {
var name: String
var id: Int
var favoriteToy: Toy
enum CodingKeys: CodingKey {
case name, id, gift
}
enum GiftKeys: CodingKey {
case toy
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
// 在外層container基礎(chǔ)上聲明嵌套container
var giftContainer = container.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
try giftContainer.encode(favoriteToy, forKey: .toy)
}
}
extension Employee: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
// 從外層container中獲取嵌套container
let giftContainer = try container.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
}
}
Date
JSONEncoder和JSONDecoder可以定制dateStrategy,系統(tǒng)提供了幾種策略如secondsSince1970``millisecondsSince1970,默認(rèn)使用deferredToDate。但日期的表示往往視實(shí)際項(xiàng)目而定,這時可以通過extension拓展DateFormatter
extension DateFormatter {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
return formatter
}()
}
// ......
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
.formatted策略只是按String的方式處理Date,DateEncodingStrategy的策略實(shí)際上有很多種,以應(yīng)付各種格式的日期
/// The strategy to use for encoding `Date` values.
public enum DateEncodingStrategy {
/// Defer to `Date` for choosing an encoding. This is the default strategy.
case deferredToDate
/// Encode the `Date` as a UNIX timestamp (as a JSON number).
case secondsSince1970
/// Encode the `Date` as UNIX millisecond timestamp (as a JSON number).
case millisecondsSince1970
/// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
case iso8601
/// Encode the `Date` as a string formatted by the given formatter.
case formatted(DateFormatter)
/// Encode the `Date` as a custom value encoded by the given closure.
///
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
case custom((Date, Encoder) throws -> Void)
}
對于客戶端來說,尤其是Decode,通常應(yīng)該覆蓋可能的各種格式,譬如:
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
// possible date strings: "2016-05-01", "2016-07-04T17:37:21.119229Z", "2018-05-20T15:00:00Z"
let len = dateStr.count
var date: Date? = nil
if len == 10 {
date = dateNoTimeFormatter.date(from: dateStr)
} else if len == 20 {
date = isoDateFormatter.date(from: dateStr)
} else {
date = self.serverFullDateFormatter.date(from: dateStr)
}
guard let date_ = date else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateStr)")
}
print("DATE DECODER \(dateStr) to \(date_)")
return date_
})
return decoder
})
Subclasses
如果JSON數(shù)據(jù)結(jié)構(gòu)是這樣的:
{
"toy" : {
"name" : "Teddy Bear"
},
"employee" : {
"name" : "John Appleseed",
"id" : 7
},
"birthday" : 580794178.33482599
}
如果把employee字段包含的內(nèi)容看做一個基礎(chǔ)父類BasicEmployee,加入的toy和birthday其實(shí)是對BasicEmployee的拓展,當(dāng)子類想利用父類已有的encode 和 decode方法:
class GiftEmployee: BasicEmployee {
var birthday: Date
var toy: Toy
enum CodingKeys: CodingKey {
case employee, birthday, toy
}
init(name: String, id: Int, birthday: Date, toy: Toy) {
self.birthday = birthday
self.toy = toy
super.init(name: name, id: id)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
birthday = try container.decode(Date.self, forKey: .birthday)
toy = try container.decode(Toy.self, forKey: .toy)
// 生成父類的decoder用于傳給父類的init方法
let baseDecoder = try container.superDecoder(forKey: .employee)
try super.init(from: baseDecoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(birthday, forKey: .birthday)
try container.encode(toy, forKey: .toy)
// 生成父類的encoder用于傳給父類的encode方法
let baseEncoder = container.superEncoder(forKey: .employee)
try super.encode(to: baseEncoder)
}
}
Handling Arrays With Mixed Types
如果JSON數(shù)據(jù)結(jié)構(gòu)是這樣的:
[
{
"name" : "John Appleseed",
"id" : 7
},
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : 580797832.94787002,
"toy" : {
"name" : "Teddy Bear"
}
}
]
是一個混合了不同類型的數(shù)組,解決方案:
struct Toy: Codable {
var name: String
}
let toy = Toy(name: "Teddy Bear")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
enum AnyEmployee: Encodable {
case defaultEmployee(String, Int)
case customEmployee(String, Int, Date, Toy)
case noEmployee
enum CodingKeys: CodingKey {
case name, id, birthday, toy
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .defaultEmployee(let name, let id):
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
case .customEmployee(let name, let id, let birthday, let toy):
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encode(birthday, forKey: .birthday)
try container.encode(toy, forKey: .toy)
default:
let context = EncodingError.Context(codingPath: encoder.codingPath,
debugDescription: "Invalid employee!")
throw EncodingError.invalidValue(self, context)
}
}
}
extension AnyEmployee: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let containerKeys = Set(container.allKeys)
let defaultKeys = Set<CodingKeys>([.name, .id])
let customKeys = Set<CodingKeys>([.name, .id, .birthday, .toy])
switch containerKeys {
case defaultKeys:
let name = try container.decode(String.self, forKey: .name)
let id = try container.decode(Int.self, forKey: .id)
self = .defaultEmployee(name, id)
case customKeys:
let name = try container.decode(String.self, forKey: .name)
let id = try container.decode(Int.self, forKey: .id)
let birthday = try container.decode(Date.self, forKey: .birthday)
let toy = try container.decode(Toy.self, forKey: .toy)
self = .customEmployee(name, id, birthday, toy)
default:
self = .noEmployee
}
}
}
let employees = [AnyEmployee.defaultEmployee("John Appleseed", 7),
AnyEmployee.customEmployee("John Appleseed", 7, Date(), toy)]
let employeesData = try encoder.encode(employees)
let employeesString = String(data: employeesData, encoding: .utf8)!
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)
Array
如果JSON直接下發(fā)了一個沒有key的數(shù)組,如:
[
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
就需要用到unkeyedContainer
struct Toy: Codable {
var name: String
}
let toy = Toy(name: "Teddy Bear")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
struct Label: Encodable {
var toy: Toy
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(toy.name.lowercased())
try container.encode(toy.name.uppercased())
try container.encode(toy.name)
}
}
extension Label: Decodable {
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var name = ""
while !container.isAtEnd {
name = try container.decode(String.self)
}
toy = Toy(name: name)
}
}
let label = Label(toy: toy)
let labelData = try encoder.encode(label)
let labelString = String(data: labelData, encoding: .utf8)!
let sameLabel = try decoder.decode(Label.self, from: labelData)
Array Within Objects
假如數(shù)據(jù)結(jié)構(gòu)是這樣:
{
"name" : "Teddy Bear",
"label" : [
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
}
需要使用nestedUnkeyedContainer
let encoder = JSONEncoder()
let decoder = JSONDecoder()
struct Toy: Encodable {
var name: String
var label: String
enum CodingKeys: CodingKey {
case name, label
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var labelContainer = container.nestedUnkeyedContainer(forKey: .label)
try labelContainer.encode(name.lowercased())
try labelContainer.encode(name.uppercased())
try labelContainer.encode(name)
}
}
extension Toy: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
var labelContainer = try container.nestedUnkeyedContainer(forKey: .label)
var labelName = ""
while !labelContainer.isAtEnd {
labelName = try labelContainer.decode(String.self)
}
label = labelName
}
}
let toy = Toy(name: "Teddy Bear", label: "Teddy Bear")
let data = try encoder.encode(toy)
let string = String(data: data, encoding: .utf8)!
let sameToy = try decoder.decode(Toy.self, from: data)