原文:https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift
基础语法
Swift将 encoding和 decoding能力内置在了系统库(Codable协议)中,不需要像OC一样编写从JSON String 到 对象的decoding方法(及其逆方法),我们所需要做的只有:
- 遵守Codable协议
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()
- 执行编码和解码
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协议的能力
嵌套编码
一段用utf8编码过得JSON下发到客户端,实际上就相当于上述代码中的string
,格式化处理后相当于:
{
"name" : "John Appleseed",
"id" : 7,
"favoriteToy" : {
"name" : "Teddy Bear"
}
}
对于favoriteToy
字段下嵌套的可以对应到Toy
的JSON结构,由于Toy
类型也遵守了Codable协议,也会被正确转为Toy
对象
Snake Case 和 Camel Case之间的转换
可以帮助解决,客户端常用Camel Case 但 API可能使用Snake Case的问题:
encoder.keyEncodeingStrategy = .convertToSnakeCase
decoder.keyDecodingStratefgy = .convertFromSnakeCase
猜测内部会对key先做一层转换再去encode/decode
自定义JSON Key
可能出现客户端和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
枚举的变量才会被解析/编码,所以即使不需要自定义,需要的变量名仍要写进去
处理与客户端不同的JSON数据结构
Flat JSON
假如API把JSON都改成Flat的了:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : "Teddy Bear"
}
和Employee
数据结构不一样,客户端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用指定键存储内容
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encode(favoriteToy.name, forKey: .gift) // 关键:指定key为gift去encode favoriteToy的内容
}
}
// 写在extension里是为了保持Swift struct的member-wise initializer, 如果在struct的主定义里写了init方法就会导致失效
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的数据结构又改成了这样:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"toy" : {
"name" : "Teddy Bear"
}
}
}
又和Employee
结构不一样,还是得像flat json的情况一样,重写encode和decode(init)方法,变化点:嵌套层加一个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基础上声明嵌套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,系统提供了几种策略如secondsSince1970``millisecondsSince1970
,默认使用deferredToDate
。但日期的表示往往视实际项目而定,这时可以通过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
的策略实际上有很多种,以应付各种格式的日期
/// 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,通常应该覆盖可能的各种格式,譬如:
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数据结构是这样的:
{
"toy" : {
"name" : "Teddy Bear"
},
"employee" : {
"name" : "John Appleseed",
"id" : 7
},
"birthday" : 580794178.33482599
}
如果把employee字段包含的内容看做一个基础父类BasicEmployee,加入的toy和birthday其实是对BasicEmployee的拓展,当子类想利用父类已有的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数据结构是这样的:
[
{
"name" : "John Appleseed",
"id" : 7
},
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : 580797832.94787002,
"toy" : {
"name" : "Teddy Bear"
}
}
]
是一个混合了不同类型的数组,解决方案:
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直接下发了一个没有key的数组,如:
[
"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
假如数据结构是这样:
{
"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)