JSONEncoder / JSONDecoder
一个类型通过声明自己遵守 Encodable 和/或 Decodable 协议,来表明可以被序列化和/或反序列化。这两个协议都只约束了一个方法,其中:Encodable 约束了 encode(to:),它定义了一个类型如何对自身进行编码;而 Decodable 则约束了一个初始化方法,用来从序列化的数据中创建实例:
/// 一个类型可以将自身编码为某种外部表示形式。
public protocol Encodable {
/// 将值编码到给定的 encoder 中。
public func encode(to encoder: Encoder) throws
}
/// 一个类型可以从某种外部表示形式中解码得到自身。
public protocol Decodable {
/// 从给定的 decoder 中解码来创建新的实例。
public init(from decoder: Decoder) throws
}
Encoding / Decoding
struct SPUserModel: Codable {
var name: String
var contact: SPContactModel?
}
struct SPContactModel: Codable {
var mobileTelephone = ""
var fixedTelephone = ""
}
Encoding
let models = [SPUserModel(name: "zhangsan", contact: SPContactModel(mobileTelephone: "138xxxxxxxx", fixedTelephone: "010-xxxxxxx")),
SPUserModel(name: "lisi", contact: SPContactModel(mobileTelephone: "135xxxxxxxx", fixedTelephone: "020-xxxxxxx"))]
do {
let encoder = JSONEncoder()
let jsonData = try encoder.encode(models)
let jsonString = String(decoding: jsonData, as: UTF8.self)
dump(jsonString)
} catch { }
Decoding
do {
let decoder = JSONDecoder()
let decoded = try decoder.decode([SPUserModel].self, from: jsonData)
dump(decoded)
} catch { }
合成的代码
Coding Keys
在 SPUserModel
里,编译器会生成一个叫做 CodingKeys 的私有枚举类型。这个枚举包含的成员与结构体中的存储属性一一对应。
private enum CodingKeys: String, CodingKey {
case name
case contact
}
encode(to:) 方法
下面是编译器为 SPUserModel
结构体生成的 encode(to:) 方法:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(contact, forKey: .contact)
}
init(from:) 初始化方法
当我们调用 try decoder.decode([SPUserModel].self, from: jsonData)
时,解码器会按照我们传入的类型 (这里是 [SPUserModel]),使用 Decodable 中定义的初始化方法创建一个该类型的实例。
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
contact = try container.decode(SPContactModel.self, forKey: .contact)
}
手动遵守协议
自定义 Coding Keys
我们可以创建自定义的 CodingKeys 枚举,在这个枚举中,我们可以:
- 在编码后的输出中,用明确指定的字符串值重命名字段。
- 将某个键从枚举中移除,以此跳过与之对应字段。
想要设置一个不同的名字,我们需要明确将枚举的底层类型设置为 String。例如, API
数据某个字段 name
更改为与模型不匹配的字段 username
,则需要自定义编码键,添加以下代码,枚举 CodingKeys
中包含 SPUserModel
模型中所有的属性,如此则可以正常解码。
如果枚举里没有包含 name
键,因此编码时 name
将会被跳过,只有 contact
会被编码,被跳过的属性必须赋个默认值,不然将会编译失败。
let json = """
[{
"username": "zhangsan",
"contact": {"mobileTelephone": "138xxxxxxxx",
"fixedTelephone": "010-xxxxxxx"
}
},
{
"username": "lisi",
"contact": {"mobileTelephone": "138xxxxxxxx",
"fixedTelephone": "010-xxxxxxx"
}
}]
"""
struct SPUserModel: Codable {
var name = ""
var contact: SPContactModel?
private enum CodingKeys: String, CodingKey {
case name = "username"
case contact
}
}
struct SPContactModel: Codable {
var mobileTelephone = ""
var fixedTelephone = ""
}
do {
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
dump(decoded)
} catch { }
自定义的 encode(to:) 和 init(from:) 实现
JSONEncoder
和 JSONDecoder
默认就可以处理可选值。当目标类型中的一个属性是可选值,如果数据中对应的值不存在的话,解码器将会正确地跳过这个属性。如下面的 contact
属性
let json = """
[{
"name": "zhangsan"
},
{
"name": "lisi"
}]
"""
struct SPUserModel: Codable {
var name = ""
var contact: SPContactModel?
}
struct SPContactModel: Codable {
var mobileTelephone = ""
var fixedTelephone = ""
}
如果数据和所期待的形式不同,则解码错误。 比如给 contact
对象一个空json对象
let json = """
[{
"name": "zhangsan",
"contact": { }
},
{
"name": "lisi",
"contact": { }
}]
"""
error: The data couldn’t be read because it is missing.
do {
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
} catch {
//The data couldn’t be read because it is missing.
print(error.localizedDescription)
}
重载 Decodable 的初始化方法 init(from:)
,明确地捕获我们所期待的错误,解码器就可以成功地解码这个错误的 JSON 了
struct SPUserModel: Codable {
var name = ""
var contact: SPContactModel?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
do {
self.contact = try container.decodeIfPresent(SPContactModel.self, forKey: .contact)
} catch DecodingError.keyNotFound {
self.contact = nil
}
}
}
struct SPContactModel: Codable {
var mobileTelephone = ""
var fixedTelephone = ""
}
常见的编码任务
让其他人的代码满足 Codable
假如 SPUserModel
中存在并不满足 Codable 协议的类,比如 CLLocationCoordinate2D
,编译器现在会 (正确地) 抱怨说它无法为 SPUserModel
自动生成实现 Codable 的代码,因为它的 coordinate
属性不再是遵从 Codable 的类型了。
struct SPUserModel: Codable {
var name = ""
var coordinate: CLLocationCoordinate2D
}
解决办法1
struct SPUserModel: Codable {
var name = ""
var coordinate: CLLocationCoordinate2D
private enum CodingKeys: String, CodingKey {
case name
case latitude = "lat"
case longitude = "lon"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
// 分别编码纬度和经度
try container.encode(coordinate.latitude, forKey: .latitude)
try container.encode(coordinate.longitude, forKey: .longitude)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// 从纬度和经度重新构建 CLLocationCoordinate2D
self.coordinate = CLLocationCoordinate2D (
latitude: try container.decode(Double.self, forKey: .latitude),
longitude: try container.decode(Double.self, forKey: .longitude)
)
}
}
let json = """
[{
"name": "zhangsan",
"lat": 312312313,
"lon": 3452423424
},
{
"name": "lisi",
"lat": 123132343,
"lon": 3453432423
}]
"""
do {
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
//Optional(__C.CLLocationCoordinate2D(latitude: 312312313.0, longitude: 3452423424.0))
dump(decoded)
} catch { }
解决办法2:嵌套容器
struct SPUserModel: Codable {
var name = ""
var coordinate: CLLocationCoordinate2D
private enum CodingKeys: String, CodingKey {
case name
case coordinate
}
// 嵌套容器的编码键
private enum CoordinateCodingKeys: CodingKey {
case latitude
case longitude
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var coordinateContainer = container.nestedContainer(keyedBy: CoordinateCodingKeys.self, forKey: .coordinate)
try coordinateContainer.encode(coordinate.latitude, forKey: .latitude)
try coordinateContainer.encode(coordinate.longitude, forKey: .longitude)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let coordinateContainer = try container.nestedContainer(keyedBy: CoordinateCodingKeys.self, forKey: .coordinate)
self.coordinate = CLLocationCoordinate2D (
latitude: try coordinateContainer.decode(Double.self, forKey: .latitude),
longitude: try coordinateContainer.decode(Double.self, forKey: .longitude)
)
}
}
let json = """
[{
"name": "zhangsan",
"coordinate": {
"latitude": 279886268,
"longitude": 123678613
}
},
{
"name": "lisi",
"coordinate": {
"latitude": 221311,
"longitude": 67868
}
}]
"""
do {
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
//Optional(__C.CLLocationCoordinate2D(latitude: 279886268.0, longitude: 123678613.0))
dump(decoded)
} catch { }
解决办法3
struct SPCoordinate: Codable {
var latitude: Double
var longitude: Double
}
struct SPUserModel: Codable {
var name: String
private var _coordinate: SPCoordinate
var coordinate: CLLocationCoordinate2D {
get {
return CLLocationCoordinate2D(latitude: _coordinate.latitude,
longitude: _coordinate.longitude)
}
set {
_coordinate = SPCoordinate(latitude: newValue.latitude,
longitude: newValue.longitude)
}
}
private enum CodingKeys: String, CodingKey {
case name
case _coordinate = "coordinate"
}
}
let json = """
[{
"name": "zhangsan",
"coordinate": {
"latitude": 279886268,
"longitude": 123678613
}
},
{
"name": "lisi",
"coordinate": {
"latitude": 221311,
"longitude": 67868
}
}]
"""
do {
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
//Optional(__C.CLLocationCoordinate2D(latitude: 279886268.0, longitude: 123678613.0))
dump(decoded)
} catch { }
tips
1. 属性样式转换(mobileTelephone -> mobile_telephone)
假如 API
数据某个字段 mobileTelephone
更改为 mobile_telephone
样式,则会出现错误 :
let json = """
[{
"name": "zhangsan",
"contact": {"mobile_telephone": "138xxxxxxxx",
"fixed_telephone": "010-xxxxxxx"
}
},
{
"name": "lisi",
"contact": {"mobile_telephone": "138xxxxxxxx",
"fixed_telephone": "010-xxxxxxx"
}
}]
"""
do {
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
print(decoded.first?.contact?.fixedTelephone)
} catch {
//error: The data couldn’t be read because it is missing.
print(error.localizedDescription)
}
struct SPUserModel: Codable {
var name: String
var contact: SPContactModel?
}
struct SPContactModel: Codable {
var mobileTelephone = ""
var fixedTelephone = ""
}
在需要编码和解码的地方添加以下代码
encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase
2.嵌套类型改变
假如 API
数据嵌套类型更改,而我们仍然希望使用嵌套。
let json = """
[{
"name": "zhangsan",
"mobile_telephone": "138xxxxxxxx",
"fixed_telephone": "010-xxxxxxx"
},
{
"name": "lisi",
"mobile_telephone": "138xxxxxxxx",
"fixed_telephone": "010-xxxxxxx"
}]
"""
do {
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
} catch { }
struct SPUserModel: Codable {
var name: String
var contact: SPContactModel?
private enum CodingKeys: CodingKey {
case name
case mobileTelephone
case fixedTelephone
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(contact?.mobileTelephone, forKey: .mobileTelephone)
try container.encode(contact?.fixedTelephone, forKey: .fixedTelephone)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.contact = SPContactModel (
mobileTelephone: try container.decode(String.self, forKey: .mobileTelephone),
fixedTelephone: try container.decode(String.self, forKey: .fixedTelephone)
)
}
}
struct SPContactModel: Codable {
var mobileTelephone = ""
var fixedTelephone = ""
}
3.日期的编解码
encoder.dateEncodingStrategy = .formatted(<#T##DateFormatter#>)
decoder.dateDecodingStrategy = .formatted(<#T##DateFormatter#>)
假如我们希望日期的格式为 "yyyy-MM-dd"
extension DateFormatter {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}
struct SPUserModel: Codable {
var name: String
var birthday: Date?
}
需要设置解编码的 dateEncodingStrategy
属性
let models = [SPUserModel(name: "zhangsan", birthday: Date()),
SPUserModel(name: "lisi", birthday: Date())]
do {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
let jsonData = try encoder.encode(models)
let jsonString = String(decoding: jsonData, as: UTF8.self)
} catch { }
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
let decoded = try decoder.decode([SPUserModel].self, from: self.jsonData)
} catch { }
PropertyListEncoder / PropertyListDecoder
class SPUserModel: NSObject, Codable {
var name: String
var address: String
init(name: String, address: String) {
self.name = name
self.address = address
}
}
Encoding
let models = [SPUserModel(name: "zhangsan", address: "beijing"),
SPUserModel(name: "lisi", address: "shanghai")]
do {
let data = try PropertyListEncoder().encode(models)
let data2 = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
UserDefaults.standard.set(data2, forKey: "key")
} catch { }
Decoding
guard let data = UserDefaults.standard.object(forKey: "key") else { return }
do {
let data2 = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [SPUserModel.self], from: data as! Data)
let model = try PropertyListDecoder().decode([SPUserModel].self, from: data2 as! Data)
dump(model)
} catch { }
--- ---
Encoding
let models = [SPUserModel(name: "zhangsan5", address: "beijing"),
SPUserModel(name: "lisi3", address: "shanghai")]
do {
let data = try PropertyListEncoder().encode(models)
NSKeyedArchiver.archiveRootObject(data, toFile: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + "/info")
} catch { }
Decoding
guard let data = NSKeyedUnarchiver.unarchiveObject(withFile: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + "/info") else { return }
do {
let model = try PropertyListDecoder().decode([SPUserModel].self, from: data as! Data)
} catch { }