Swift - JSON 從入門到精通
在 iOS 中最常見的工作是將數據保存起來並通過網絡傳輸。但是在這之前,你需要將數據通過編碼
或序列化
轉換成合適的格式。
同樣的,在你使用這些數據之前,你也需要將其轉換成合適的格式。這個相反的過程被稱爲解碼
或反序列化
。
在這個教程中,你將學習到所有使用 Swift 進行編解碼所需要的知識。包括這些:
-
在
蛇形命名
和駝峯命名
格式之間轉換 -
自定義
Coding keys
-
使用
keyed
,unkeyed
和nested
容器 -
處理
嵌套類型
,日期類型
以及子類
這確實有點多,是時候開始動手了!
開始動手
關注 iOS 大全 公號,發消息 Codable 即可獲取所需資源。
下載完成後,starter 是該教程使用的版本。final 是最終完成的版本。
我們打開本節代碼Nested types
。使Toy
和Employee
遵循Codable
協議:
struct Toy: Codable {
...
}
struct Employee: Codable {
...
}
Codable
本身並不是一個協議,它只是另外兩個協議的別名:Encodable
和Decodable
。你也行已經猜到了,這兩個協議就是代表那些可以被編解碼的類型。
你無需再做其他事情,因爲Toy
和Employee
的所有存儲屬性都是Codable
的。Swift 標準庫中大多數類型(例如String
、URL
)都是支持Codable
的。
添加一個JSONEncoder
和JSONDecoder
來處理toys
和employees
的編解碼:
let encoder = JSONEncoder()
let decoder = JSONDecoder()
操作 JSON 我們只需做這些!下面進入第一個挑戰!
編解碼嵌套類型
Employee
包含了一個Toy
屬性(這是個嵌套類型)。編碼後的 JSON 結構和Employee
結構體保持一致:
{
"name" : "John Appleseed",
"id" : 7,
"favoriteToy" : {
"name" : "Teddy Bear"
}
}
public struct Employee: Codable {
var name: String
var id: Int
var favoriteToy: Toy
}
JSON 數據將name
嵌套在favoriteToy
之中,並且所有的 JSON 字段名與Toy
和Employee
的存儲屬性名相同,所以基於結構體的類型體系,JSON 的結構很容易理解。
如果屬性名稱和 JSON 的字段名都相同,並且屬性都是Codable
的,那麼我們可以很容易的將 JSON 轉換爲數據模型,或者反過來。現在來試一試:
// 1
let data = try encoder.encode(employee)
// 2
let string = String(data: data, encoding: .utf8)!
這裏做了 2 件事:
-
將
employee
使用encode(_:)
編碼成 JSON。是不是很簡單! -
從上一步的
data
中創建 String,一遍可以查看其內容。
這裏的編碼過程會產生合法的數據,所以我們可以使用它重新創建employee
:
let sameEmployee = try decoder.decode(Employee.self, from: data)
好了,可以開始下一個挑戰了!
在蛇形命名
和駝峯命名
格式之間轉換
現在,假設 JSON 的鍵名從駝峯格式(這樣looksLikeThis
)轉換成了蛇形格式(這樣looks_like_this_instead
)。但是,Toy
和Employee
的存儲屬性只能使用駝峯格式。幸運的是Foundation
考慮到了這種情況。
打開本節代碼Snake case vs camel case
,在編解碼器創建之後使用之前的位置添加下面的代碼:
encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase
運行代碼,檢查snakeString
,編碼後的employee
產生下面的內容:
{
"name" : "John Appleseed",
"id" : 7,
"favorite_toy" : {
"name" : "Teddy Bear"
}
}
自定義 Coding keys
現在,假設 JOSN 的格式再一次改變,其使用的字段名和Toy
和Employee
中存儲屬性名不一致了:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"name" : "Teddy Bear"
}
}
可以看到,這裏使用gift
代替了原來的favoriteToy
。這種情況我們需要自定義Coding keys
。在我們的類型中添加一個特殊的枚舉類型。打開本節代碼Custom coding keys
,在Employee
中添加下面的代碼:
enum CodingKeys: String, CodingKey {
case name, id, favoriteToy = "gift"
}
這個特殊的枚舉遵循了CodingKey
協議,並使用String
類型的原始值。在這裏我們可以讓favoriteToy
和gift
一一對應起來。
在編解碼過程中,只會操作出現在枚舉中的cases
,所以即使那些不需要指定一一對應的屬性,也需要在枚舉中包含,就像這裏的name
和id
。
運行playground
,然後查看string
的值,你會發現 JSON 字段名不在依賴存儲屬性名稱,這得益於自定義的Coding keys
。
繼續下一個挑戰!
處理扁平化
的 JSON
現在,JSON 的格式變成下面這樣:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : "Teddy Bear"
}
這裏不在有嵌套
結構,和我們的模型結構不一致了。這種情況我們需要自定義編解碼過程。
打開本節代碼Keyed containers
。這裏有個Employee
類型,它遵循了Encodable
。同時我們使用extension
讓它遵循了Decodable
。
這樣做的好處是,可以保留結構體的逐一成員構造器
。如果我們在定義Employee
時讓它遵循Decodable
,它將失去這個構造器。添加下面的代碼到Employee
中:
// 1
enum CodingKeys: CodingKey {
case name, id, gift
}
func encode(to encoder: Encoder) throws {
// 2
var container = encoder.container(keyedBy: CodingKeys.self)
// 3
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
// 4
try container.encode(favoriteToy.name, forKey: .gift)
}
在之前簡單 (指屬性名和鍵名一一對應且嵌套層級相同) 的示例中,encode(to:)
方法由編譯器自動實現了。現在我們需要手動實現。
-
創建
CodingKeys
表示 JSON 的字段。因爲我們沒有做任何的關係映射,所以不必聲明它的原始類型爲String
。 -
從
encoder
中獲取KeyedEncodingContainer
容器。這就像一個字典,我們可以存儲屬性的值到其中,這樣就進行了編碼。 -
編碼
name
和id
屬性到容器中。 -
使用
gift
鍵,直接將toy
的名字編碼到容器中。
運行playground
,然後查看string
的值,你會發現它符合上面 JSON 的格式。我們可以選擇使用什麼字段名編碼一個屬性值,這給了我們很大的靈活性。
和編碼過程類似,簡單版本的init(from:)
方法可以由編譯器自動實現。但是這裏我們需要手動實現,使用下面的代碼替換fatalError("To do")
:
// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
// 2
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
// 3
let gift = try container.decode(String.self, forKey: .gift)
favoriteToy = Toy(name: gift)
然後添加下面的代碼,就可以從 JSON 中重新創建employee
:
let sameEmployee = try decoder.decode(Employee.self, from: data)
處理多級嵌套的 JSON
現在,JSON 的格式變成下面這樣:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"toy" : {
"name" : "Teddy Bear"
}
}
}
name
字段在toy
字段中,而toy
又在gift
字段中。如何解析成我們定義的數據模型呢?
打開本節代碼Nested keyed containers
,添加下面的代碼到Employee
:
// 1
enum CodingKeys: CodingKey {
case name, id, gift
}
// 2
enum GiftKeys: CodingKey {
case toy
}
// 3
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)
// 4
var giftContainer = container
.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
try giftContainer.encode(favoriteToy, forKey: .toy)
}
這裏做了幾件事:
-
創建頂層的
CodingKeys
-
創建用於解析
gift
字段的CodingKeys
,後續使用它創建容器 -
使用頂層容器編碼
name
和id
-
使用
nestedContainer(keyedBy:forKey:)
方法獲取用於編碼gift
字段的容器,並將favoriteToy
編碼進去
運行並查看string
的值,你會發現 JSON 的格式符合預期。
解碼過程也很類似。添加下面的代碼:
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 giftContainer = try container
.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
}
}
let sameEmployee = try decoder.decode(Employee.self, from: nestedData)
好了,我們已經搞定了嵌套類型的容器。並從其中解碼出了sameEmployee
。
處理日期類型
現在,JSON 裏添加了日期字段,就像下面這樣:
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : "29-05-2019",
"toy" : {
"name" : "Teddy Bear"
}
}
JSON 中並沒有標準的日期格式。在JSONEncoder
和JSONDecoder
使用日期類的timeIntervalSinceReferenceDate
方法去處理 (Date(timeIntervalSinceReferenceDate: interval)
)。
這裏我們需要指定日期轉換策略
。打開本節代碼Dates
,在try encoder.encode(employee)
之前添加下面的代碼:
// 1
extension DateFormatter {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
return formatter
}()
}
// 2
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
這裏主要做了 2 件事:
-
在
DateFormatter
的擴展中添加了格式化器
,它的格式化形式滿足 JSON 中日期的格式,並且是可以重用的。 -
設置
dateEncodingStrategy
和dateDecodingStrategy
爲.formatted(.dateFormatter)
,這樣編解碼時就會使用它去處理日期
運行並檢查dateString
的內容,你會發現它符合預期。
處理子類
現在,JSON 格式變成了下面這樣:
{
"toy" : {
"name" : "Teddy Bear"
},
"employee" : {
"name" : "John Appleseed",
"id" : 7
},
"birthday" : 580794178.33482599
}
這裏將Employee
所需信息分開了。我們打算使用BasicEmployee
去解析employee
。打開本節代碼Subclasses
,使BasicEmployee
遵循Codable
:
class BasicEmployee: Codable {
不出意外,編譯器報錯了,因爲GiftEmployee
並沒有遵循Codable
。我們繼續添加下面的代碼,就可以修正錯誤了:
// 1
enum CodingKeys: CodingKey {
case employee, birthday, toy
}
// 2
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)
// 3
let baseDecoder = try container.superDecoder(forKey: .employee)
try super.init(from: baseDecoder)
}
這裏做了 3 件事:
-
在
GiftEmployee
中添加了CodingKeys
。和JSON
中的字段名對應。 -
從
decoder
解碼出子類的屬性值。 -
創建用於解碼父類屬性的
Decoder
,然後調用父類的方法初始化父類屬性。
下面我們繼續完成GiftEmployee
的編碼方法:
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)
let baseEncoder = container.superEncoder(forKey: .employee)
try super.encode(to: baseEncoder)
}
和解碼過程類似,我們先編碼了子類的屬性,然後獲取用於編碼父類的encoder
。下面測試下結果:
let giftEmployee = GiftEmployee(name: "John Appleseed", id: 7, birthday: Date(), toy: toy)
let giftData = try encoder.encode(giftEmployee)
let giftString = String(data: giftData, encoding: .utf8)!
let sameGiftEmployee = try decoder.decode(GiftEmployee.self, from: giftData)
運行並檢查giftString
,你會發現其內容符合預期。學習了本節,你就可以處理更復雜的繼承數據模型了。
處理混合類型的數組
現在,JSON 格式變成了下面這樣:
[
{
"name" : "John Appleseed",
"id" : 7
},
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : 580797832.94787002,
"toy" : {
"name" : "Teddy Bear"
}
}
]
這是個 JSON 數組,但是其內部元素格式並不一致。打開本節代碼Polymorphic types
,可以看到這裏使用枚舉定義了不同類型的數據。
首先,我們讓AnyEmployee
遵循Encodable
協議:
enum AnyEmployee: Encodable { ... }
繼續在AnyEmployee
中添加下面的代碼:
// 1
enum CodingKeys: CodingKey {
case name, id, birthday, toy
}
// 2
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)
case .noEmployee:
let context = EncodingError.Context(codingPath: encoder.codingPath,
debugDescription: "Invalid employee!")
throw EncodingError.invalidValue(self, context)
}
}
這裏我們主要做了兩件事:
-
定義了所有可能的鍵。
-
根據不同類型,對數據進行編碼。
在代碼的最後添加下面的內容來進行測試:
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)!
接下來的編碼過程有點複雜。繼續添加下面的代碼:
extension AnyEmployee: Decodable {
init(from decoder: Decoder) throws {
// 1
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])
// 2
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
}
}
}
// 3
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)
解釋下上面的代碼:
-
獲取
KeydContainer
,並獲取其所有鍵。 -
根據不同的鍵,實行不同的解析策略
-
從
employeesData
中解碼出[AnyEmployee]
個人感覺若數組中的元素可以用同一模型來表示,只是字段可能爲空時,直接將模型字段設爲可選。當然這裏也提供瞭解析不同模型的思路。
處理數組
現在,我們有如下格式 JSON:
[
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
這裏是一個數組,並且其大小寫各不相同。此時我們不需要任何CodingKey
,只需使用unkeyed container
。
打開本節代碼Unkeyed containers
,添加下面的代碼到Label
結構體中:
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)
}
UnkeyedEncodingContainer
和之前用到的KeyedEncodingContainer
相似,但是它不需要CodingKey
,因爲它將編碼數據寫入 JSON 數組中。這裏我們編碼了 3 中不同的字符串到其中。
繼續解碼:
extension Label: Decodable {
// 1
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 sameLabel = try decoder.decode(Label.self, from: labelData)
這裏主要是獲取decoder.unkeyedContainer
,獲取容器中最後一個值來初始化name
。
處理嵌套在對象中的數組
現在我們有如下格式 JSON:
{
"name" : "Teddy Bear",
"label" : [
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
}
這次,標籤對應在了label
字段下。我們需要使用nested unkeyed containers
去進行編解碼。
打開本節代碼Nested unkeyed containers
,在Toy
中添加下面的代碼:
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)
}
這裏我們創建了一個nested unkeyed container
,並填充了 3 個字符串。運行代碼,並查看string
的值,可以看到預期結果。
繼續添加下面的代碼進行解碼:
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 sameToy = try decoder.decode(Toy.self, from: data)
這裏,我們像之前一樣,使用unkeyed container
的最後一個值初始化label
字段,只不過獲取的是嵌套的容器。
處理可選字段
最後,我們的模型中的屬性也可以是可選類型,container
也提供了對應的編解碼方法:
encodeIfPresent(value, forKey: key)
decodeIfPresent(type, forKey: key)
總結
今天我們由淺入深的學習瞭如何在Swift
中處理JSON
。其中自定義Coding keys
、處理子類
等部分需要重點理解。希望對大家有所幫助。
原文鏈接:
https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift
翻譯:泥瓦罐
https://juejin.cn/post/7044871144091942925
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gXrHkglbtijnIjARCvvddw