Swift - JSON 從入門到精通

在 iOS 中最常見的工作是將數據保存起來並通過網絡傳輸。但是在這之前,你需要將數據通過編碼序列化轉換成合適的格式。

同樣的,在你使用這些數據之前,你也需要將其轉換成合適的格式。這個相反的過程被稱爲解碼反序列化


在這個教程中,你將學習到所有使用 Swift 進行編解碼所需要的知識。包括這些:

  1. 蛇形命名駝峯命名格式之間轉換

  2. 自定義Coding keys

  3. 使用keyed,unkeyednested容器

  4. 處理嵌套類型,日期類型以及子類

這確實有點多,是時候開始動手了!

開始動手

關注 iOS 大全 公號,發消息 Codable 即可獲取所需資源。

下載完成後,starter 是該教程使用的版本。final 是最終完成的版本。

我們打開本節代碼Nested types。使ToyEmployee遵循Codable協議:

struct Toy: Codable {
  ...
}
struct Employee: Codable {
  ...
}

Codable本身並不是一個協議,它只是另外兩個協議的別名:EncodableDecodable。你也行已經猜到了,這兩個協議就是代表那些可以被編解碼的類型。

你無需再做其他事情,因爲ToyEmployee的所有存儲屬性都是Codable的。Swift 標準庫中大多數類型(例如StringURL)都是支持Codable的。

添加一個JSONEncoderJSONDecoder來處理toysemployees的編解碼:

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 字段名與ToyEmployee的存儲屬性名相同,所以基於結構體的類型體系,JSON 的結構很容易理解。

如果屬性名稱和 JSON 的字段名都相同,並且屬性都是Codable的,那麼我們可以很容易的將 JSON 轉換爲數據模型,或者反過來。現在來試一試:

// 1
let data = try encoder.encode(employee)
// 2
let string = String(data: data, encoding: .utf8)!

這裏做了 2 件事:

  1. employee使用encode(_:)編碼成 JSON。是不是很簡單!

  2. 從上一步的data中創建 String,一遍可以查看其內容。

這裏的編碼過程會產生合法的數據,所以我們可以使用它重新創建employee

let sameEmployee = try decoder.decode(Employee.self, from: data)

好了,可以開始下一個挑戰了!

蛇形命名駝峯命名格式之間轉換

現在,假設 JSON 的鍵名從駝峯格式(這樣looksLikeThis)轉換成了蛇形格式(這樣looks_like_this_instead)。但是,ToyEmployee的存儲屬性只能使用駝峯格式。幸運的是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 的格式再一次改變,其使用的字段名和ToyEmployee中存儲屬性名不一致了:

{
  "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類型的原始值。在這裏我們可以讓favoriteToygift一一對應起來。

在編解碼過程中,只會操作出現在枚舉中的cases,所以即使那些不需要指定一一對應的屬性,也需要在枚舉中包含,就像這裏的nameid

運行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:)方法由編譯器自動實現了。現在我們需要手動實現。

  1. 創建CodingKeys表示 JSON 的字段。因爲我們沒有做任何的關係映射,所以不必聲明它的原始類型爲String

  2. encoder中獲取KeyedEncodingContainer容器。這就像一個字典,我們可以存儲屬性的值到其中,這樣就進行了編碼。

  3. 編碼nameid屬性到容器中。

  4. 使用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)
}

這裏做了幾件事:

  1. 創建頂層的CodingKeys

  2. 創建用於解析gift字段的CodingKeys,後續使用它創建容器

  3. 使用頂層容器編碼nameid

  4. 使用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 中並沒有標準的日期格式。在JSONEncoderJSONDecoder使用日期類的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 件事:

  1. DateFormatter的擴展中添加了格式化器,它的格式化形式滿足 JSON 中日期的格式,並且是可以重用的。

  2. 設置dateEncodingStrategydateDecodingStrategy.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 件事:

  1. GiftEmployee中添加了CodingKeys。和JSON中的字段名對應。

  2. decoder解碼出子類的屬性值。

  3. 創建用於解碼父類屬性的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)
  }
}

這裏我們主要做了兩件事:

  1. 定義了所有可能的鍵。

  2. 根據不同類型,對數據進行編碼。

在代碼的最後添加下面的內容來進行測試:

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)

解釋下上面的代碼:

  1. 獲取KeydContainer,並獲取其所有鍵。

  2. 根據不同的鍵,實行不同的解析策略

  3. 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