[译]Swift 4 解析JSON最终指南

时间:2022-06-01 14:06:26

Swift 4和Foundation终于回答了如何用Swift来解析JSON的问题。

目前已经有了许多优秀的库,但是看到一个全支持的解决方案是非常令人耳目一新的,它不仅易于使用且也提供了对复杂场景进行编码和解码所需的定制。

值得注意的是,这里讨论的所有内容都适用于所有Encoder/Decoder实现,例如包括PropertyListEncoder。 如果你需要类似XML这样不同的东西,也可以创建自己的实现。 本博文的其余部分将重点讨论JSON解析,因为这是大多数iOS开发人员最相关的。

基础

如果你的JSON和对象具有相似的结构,那么你的工作非常简单。

这是一个啤酒的JSON文档示例:

{
    "name": "Endeavor",
    "abv": 8.9,
    "brewery": "Saint Arnold",
    "style": "ipa"
}

我们的Swift数据结构可能如下所示:

enum BeerStyle : String {
    case ipa
    case stout
    case kolsch
    // ...
}

struct Beer {
    let name: String
    let brewery: String
    let style: BeerStyle
}

为了将JSON字符串转换成为Beer实例,我们将类型标记为Codable。

Codable实际上就是Swift所说的一个协议组合类型,它由Encodable和Decodable组成,所以如果你只关心单向转换,你可以采用适当的协议。 这是Swift 4的一个新功能。

Codable有一个默认的实现,所以在很多情况下,你可以采用这个协议,并获得有用的默认行为。

enum BeerStyle : String, Codable {
   // ...
}

struct Beer : Codable {
   // ...
}

接着我们只需要创建一个解码器:

let jsonData = jsonString.data(encoding: .utf8)!
let decoder = JSONDecoder()
let beer = try! decoder.decode(Beer.self, for: jsonData)

就是这样! 我们已经将JSON文档解析为一个beer实例。 它不需要任何定制,因为键名和类型相互匹配。

值得注意的是,为了举例我们使用了try!,但在你的应用程序,你应该捕捉错误,并灵活处理它们。 稍后再讨论处理错误...

在这个我们造的例子里,事情完全排好了。 但是,如果类型不匹配呢?

自定义键名称

通常情况下,API使用蛇形(snake-case)命名键,这种风格与Swift属性的命名准则不匹配。

定制这个,我们需要等待Codable的默认实现一段时间。

键是由编译器生成的“CodingKeys”枚举自动处理。 此枚举符合CodingKey,它定义了我们如何将一个属性连接到编码格式的值。

要定制这些键,我们必须编写我们自己的这个实现。 对于不同于swift命名的情况,我们可以为键提供一个字符串值:

struct Beer : Codable {
      // ...
      enum CodingKeys : String, CodingKey {
          case name
          case abv = "alcohol_by_volume"
          case brewery = "brewery_name"
          case style
    }
}

如果我们将beer实例编码为JSON,可以看到执行生成新格式:

let encoder = JSONEncoder()
let data = try! encoder.encode(beer)
print(String(data: data, encoding: .utf8)!)

输出为:


{"style":"ipa","name":"Endeavor","alcohol_by_volume":8.8999996185302734,"brewery_name":"Saint Arnold"}

这个格式不是很可读。 我们可以使用outputFormatting属性自定义JSONEncoder的输出格式,让它看起来更好一些。

默认值是.compact,它生成上面的输出。 我们可以将其改为.prettyPrinted以获得更可读的输出。

encoder.outputFormatting = .prettyPrinted
{
  "style" : "ipa",
  "name" : "Endeavor",
  "alcohol_by_volume" : 8.8999996185302734,
  "brewery_name" : "Saint Arnold"
}

JSONEncoder和JSONDecoder都有很多自定义的选项。 更常见的要求之一是定制如何解析日期。

处理日期

JSON没有数据类型来表示日期,所以这些被序列化的日期表示需要得到客户端和服务器一致同意。 通常,这是使用ISO 8601日期格式化来做,然后将其序列化为一个字符串。

专业提示:nsdateformatter.com是一个不错的格式字符串的地方,包括ISO 8601格式。

其他格式可以是自引用日期以来的秒数(或毫秒),它将被序列化为JSON文档中的数字。

在过去,我们必须自己处理这个问题,为我们的数据类型提供一个字符串字段,然后使用我们自己的DateFormatter实例从字符串值封装日期,反之亦然。

使用JSONEncoder和JSONDecoder,这一切都为我们完成。 一探究竟。 默认情况下,这些将使用.deferToDate作为处理日期的样式,如下所示:

struct Foo : Encodable {
    let date: Date
}

let foo = Foo(date: Date())
try! encoder.encode(foo)
{
  "date" : 519751611.12542897
}

我们可以将其改为.iso8601格式:

encoder.dateEncodingStrategy = .iso8601
{
  "date" : "2017-06-21T15:29:32Z"
}

其他可用的JSON日期编码策略是:

  • .formatted(DateFormatter) - 当你需要支持一个非标准的日期格式字符串。 提供你自己的日期格式化实例。
  • .custom( (Date, Encoder) throws -> Void ) - 当你有自定义的东西的时候,你可以在这里传递一个块,把日期编码到提供的编码器中。
  • .millisecondsSince1970 and .secondsSince1970,在API中并不常见。 不建议使用这样的格式,因为编码表示中完全没有时区信息,这使得人们更容易做出错误的假设。

解码日期本质上是相同的选项,但是对于.custom而言,它需要.custom((Decoder)throws - > Date)的形式,所以我们得到一个解码器,我们负责将这个解码器保存为一个日期。

处理浮点数

浮点数是JSON与Swift的Float类型不匹配的另一个地方。 如果服务器返回一个无效的“NaN”作为字符串会发生什么? 什么是正货负无穷? 这些都没有映射到Swift中的任何特定值。

默认的实现是.throw,这意味着如果解码器遇到这些值,那么将会抛出错误,但是如果我们需要处理这个错误,我们可以提供映射:

{
   "a": "NaN",
   "b": "+Infinity",
   "c": "-Infinity"
}
struct Numbers : Decodable {
  let a: Float
  let b: Float
  let c: Float
}
decoder.nonConformingFloatDecodingStrategy =
  .convertFromString(
      positiveInfinity: "+Infinity",
      negativeInfinity: "-Infinity",
      nan: "NaN")

let numbers = try! decoder.decode(Numbers.self, from: jsonData)
dump(numbers)

我们得到:

▿ __lldb_expr_71.Numbers
  - a: inf
  - b: -inf
  - c: nan

你也可以用JSONEncoder的nonConformingFloatEncodingStrategy来做相反的事情。

这在大多数情况下不太可能需要,但有一天它可能派上用场。

处理数据

有时你会发现API以小数据的形式发送base64编码的字符串。

为了自动处理,你可以给JSONEncoder一个编码策略:

  • .base64
  • .custom( (Data, Encoder) throws -> Void)

要解码它,你可以给JSONDecoder提供一个解码策略:

  • .base64
  • .custom( (Decoder) throws -> Data)

显然.base64会是这里常见的选择,但是如果你需要做任何自定义的事情,你可以使用基于块的策略。

处理URL

大多数情况下,URL都可以使用。 如果对象有一个URL属性,JSON文档中匹配的键将被用来创建URL(使用URL(string:)初始化)。

给出JSON:

{
  "title": "NSDateFormatter - Easy Skeezy Date Formatting...",
  "url": "http://nsdateformatter.com"
}

我们可以将其映射到一个没有自定义的对象:

struct Webpage : Codable {
  let title: String
  let url: URL
}

包装键

通常情况下,API会包装键名,这样JSON实体始终是一个对象。

{
  "beers": [ {...} ]
}

为了在Swift中表示出来,我们可以为这个响应创建一个新类型:

struct BeerList : Codable {
    let beers: [Beer]
}

其实就是这样! 由于我们的键名匹配,Beer已经是Codable,这样就可以了。

根级别的数组

如果API返回一个数组作为根元素,则解析响应如下所示:

let decoder = JSONDecoder()
let beers = try decoder.decode([Beer].self, from: data)

请注意,我们在这里使用数组作为类型。 只要T是可解码的,Array <T>就可解码的。

处理对象包装键

下面是另一个可能会遇到的情况:数组中的每个对象都有一个键的数组响应。

[
  {
    "beer" : {
      "id": "uuid12459078214",
      "name": "Endeavor",
      "abv": 8.9,
      "brewery": "Saint Arnold",
      "style": "ipa"
    }
  }
]

你可以使用上面的包装类型方法来捕获此键,但更简单的方法是认识到这个结构是由强类型可解码实现组成。

看到了吗?

[[String:Beer]]

或者这种方式可能更可读:

Array<Dictionary<String, Beer>>

就像Array<T>是可解码的,如果K和T都是可解码的,那么Dictionary<K , T>也是可解码的。

let decoder = JSONDecoder()
let beers = try decoder.decode([[String:Beer]].self, from: data)
dump(beers)
▿ 1 element
  ▿ 1 key/value pair
    ▿ (2 elements)
      - key: "beer"
      ▿ value: __lldb_expr_37.Beer
        - name: "Endeavor"
        - brewery: "Saint Arnold"
        - abv: 8.89999962
        - style: __lldb_expr_37.BeerStyle.ipa

更复杂的嵌套响应

有时我们的API响应并不那么简单。 也许在顶层,它不仅仅是定义响应中的对象的键,而且通常你会收到多个集合,或者可能是分页信息。

例如:

{
    "meta": {
        "page": 1,
        "total_pages": 4,
        "per_page": 10,
        "total_records": 38
    },
    "breweries": [
        {
            "id": 1234,
            "name": "Saint Arnold"
        },
        {
            "id": 52892,
            "name": "Buffalo Bayou"
        }
    ]
}

我们实际上可以在Swift中嵌套类型,在对json进行编码/解码时会使用这种结构。

struct PagedBreweries : Codable {
    struct Meta : Codable {
        let page: Int
        let totalPages: Int
        let perPage: Int
        let totalRecords: Int
        enum CodingKeys : String, CodingKey {
            case page
            case totalPages = "total_pages"
            case perPage = "per_page"
            case totalRecords = "total_records"
        }
    }

    struct Brewery : Codable {
        let id: Int
        let name: String
    }

    let meta: Meta
    let breweries: [Brewery]
}

这种方法的一个巨大好处是你可以对同一类型的对象有不同的响应变化(也许在这种情况下,“brewery”在列表响应中只有id和name,但是如果你选择brewery本身可以有更多属性)。 由于这里的Brewery类型是嵌套的,我们可以在其他地方有不同的Brewery类型来解码和编码不同的结构。

更深的定制

到目前为止,我们仍然依靠Encodable和Decodable的默认实现来为我们完成繁重的工作。

这将处理大多数情况,但是为了更好地控制编码和解码,最终你必须下来自己去做。

自定义编码

首先,我们将实现编译器免费提供的自定义版本。 我们从编码开始。

extension Beer {
    func encode(to encoder: Encoder) throws {

    }
}

我也想添加几个新的字段到Beer类型,这是为了完善例子:

struct Beer : Codable {
    // ...
    let createdAt: Date
    let bottleSizes: [Float]
    let comments: String?

    enum CodingKeys: String, CodingKey {
        // ...
        case createdAt = "created_at",
        case bottleSizes = "bottle_sizes"
        case comments
    }
}

在这个方法中,我们需要取得编码器,得到一个“容器”并对其进行编码。

什么是容器?

一个容器可以是几种不同的类型之一:

  • 键容器(keyed container) - 通过键提供值。 这本质上是一本字典。
  • 非键容器 - 这提供了没有键的有序值。 在JSONEncoder中,这意味着一个数组。
  • 单值容器 - 这个输出原始值没有任何种类的包含元素。

为了编码我们的任何属性,我们首先需要获得一个容器。 看看这篇文章顶部的JSON结构,很明显我们需要一个键容器:

var container = encoder.container(keyedBy: CodingKeys.self)

这里要注意两点:

  • 容器必须是一个可变属性,因为我们将写入它,所以变量必须用var声明
  • 我们必须指定键(以及属性/键映射),以便知道我们可以将哪些键编码到此容器中

正如我们将会看到的那样,稍后会证明这是超级强大的。

接下来,我们需要将值编码到容器中。 这些调用中的任何一个都可能会引发错误,所以我们每一行都以try开始:

try container.encode(name, forKey: .name)
try container.encode(abv, forKey: .abv)
try container.encode(brewery, forKey: .brewery)
try container.encode(style, forKey: .style)
try container.encode(createdAt, forKey: .createdAt)
try container.encode(comments, forKey: .comments)
try container.encode(bottleSizes, forKey: .bottleSizes)

对于注释字段,Encodable的在可选值上默认使用encodeIfPresent。 这意味着如果键为nil,编码会丢失键。 对于API来说,这通常不是一个好的解决方案,即使它们是空值,也应包含键是一个最佳实践。 这里我们通过使用encode(_: forKey: )而不是encodeIfPresent(_:forKey :)来强制输出包含此键。

我们的bottleSizes值也是自动编码的,但是如果我们需要自定义这个,我们必须创建自己的容器。 在这里,我们处理每一项(通过舍入浮点数)并将其添加到容器中:

var sizes = container.nestedUnkeyedContainer(
      forKey: .bottleSizes)

try bottleSizes.forEach {
      try sizes.encode($0.rounded())
}

我们完成了! 请注意,这里没有谈到浮点数策略或日期格式。 实际上,这个方法完全是JSON不可知的,这是设计的一部分。 编码和解码类型是一个通用的特征,格式很容易由相关方指定。

我们编码的JSON现在看起来像这样:

{
  "comments" : null,
  "style" : "ipa",
  "brewery_name" : "Saint Arnold",
  "created_at" : "2016-05-01T12:00:00Z",
  "alcohol_by_volume" : 8.8999996185302734,
  "bottle_sizes" : [
    12,
    16
  ],
  "name" : "Endeavor"
}

这里值得注意的是我们在原始的JSON文档中开始的浮点值是8.9,但是由于浮点数在内存中表示的方式不同,所以你传入的数字不同。如果您需要特定的数字精度, 希望每次都使用NumberFormatter手动进行格式化。 尤其是,处理货币的API通常会将美分数作为整数值(可以安全地舍入),然后将其除以100得到美元值。

现在我们可以做相反的事情了。 我们来编写Decodable协议:

自定义解码

解码本质上意味着编写另一个初始化器。

extension Beer {
    init(from decoder: Decoder) throws {

    }
}

我们再次需要从解码器获取一个容器:

let container = try decoder.container(keyedBy: CodingKeys.self)

我们可以解码所有的基本属性。 在每种情况下,我们必须指定预期的类型。 如果类型不匹配,则会抛出DecodingError.TypeMismatch并且有我们可以用来判断发生了什么的信息。

let name = try container.decode(String.self, forKey: .name)
let abv = try container.decode(Float.self, forKey: .abv)
let brewery = try container.decode(String.self,
      forKey: .brewery)
let style = try container.decode(BeerStyle.self,
      forKey: .style)
let createdAt = try container.decode(Date.self,
      forKey: .createdAt)
let comments = try container.decodeIfPresent(String.self,
      forKey: .comments)

我们可以对我们的bottleSizes数组使用相同的方法,但是我们也可以用类似的方式处理每个值。 在这里,我们将值存储在新的实例之前,将它们取整:

var bottleSizesArray = try container.nestedUnkeyedContainer(forKey: .bottleSizes)
var bottleSizes: [Float] = []
while (!bottleSizesArray.isAtEnd) {
    let size = try bottleSizesArray.decode(Float.self)
    bottleSizes.append(size.rounded())
}

我们将继续从容器解码值,直到容器没有更多的元素。

现在定义了所有这些变量,我们有了调用默认初始的所有的答案:

self.init(name: name,
              brewery: brewery,
              abv: abv,
              style: style,
              createdAt: createdAt,
              bottleSizes: bottleSizes,
              comments: comments)

通过自定义实现encode(to encoder:) 和 init(from decoder:),我们可以更好地控制生成的JSON如何映射到我们的类型。

展开对象

比方说,JSON有一个我们不关心的嵌套层次。 修改上面的例子,假设abv和style被表示为:

{
   "name": "Lawnmower",
   "info": {
     "style": "kolsch",
     "abv": 4.9
   }
   // ...
}

要使用这个结构,我们必须自己实现编码和解码。

我们首先定义这些嵌套键的枚举(并将它们从主CodingKeys枚举中移除:

struct Beer : Codable {
  enum CodingKeys: String, CodingKey {
      case name
      case brewery
      case createdAt = "created_at"
      case bottleSizes = "bottle_sizes"
      case comments
      case info // <-- NEW
  }

  enum InfoCodingKeys: String, CodingKey {
      case abv
      case style
  }
}

当我们对这个值进行编码时,我们需要先获得信息容器的引用(如果你记得是一个容器)。

func encode(to encoder: Encoder) throws {
      var container = encoder.container(
          keyedBy: CodingKeys.self)

      var info = try container.nestedContainer(
          keyedBy: InfoCodingKeys.self, forKey: .info)
      try info.encode(abv, forKey: .abv)
      try info.encode(style, forKey: .style)

    // ...

对于解码的实现,我们可以做相反的事情:

init(from decoder: Decoder) throws {
    let container = try decoder.container(
          keyedBy: CodingKeys.self)

    let info = try container.nestedContainer(
          keyedBy: InfoCodingKeys.self, forKey: .info)
    let abv = try info.decode(Float.self, forKey: .abv)
    let style = try info.decode(BeerStyle.self,
          forKey: .style)

    // ...
}

现在我们可以使用编码格式的嵌套结构,但在我们的对象中展开。

创建子对象

比方说,brewery 作为一个简单的字符串来传递,但我们想保持我们单独的Brewery 类型。

{
  "name": "Endeavor",
  "brewery": "Saint Arnold",
  // ...
}

在这种情况下,我们必须提供自定义的encode(to encoder:) 和 init(from decoder:)。

func encode(to encoder: Encoder) throws {
      var container = encoder.container(keyedBy:
          CodingKeys.self)

      try encoder.encode(brewery.name, forKey: .brewery)

      // ...     
}

init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy:
          CodingKeys.self)
      let breweryName = try decoder.decode(String.self,
          forKey: .brewery)
      let brewery = Brewery(name: breweryName)

    // ...
}

继承

假设我们有以下类:

class Person : Codable {
    var name: String?
}

class Employee : Person {
    var employeeID: String?
}

我们继承Person类来获得Codable一致性,但是如果我们尝试编码Employee实例会发生什么?

let employee = Employee()
employee.employeeID = "emp123"
employee.name = "Joe"

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(employee)
print(String(data: data, encoding: .utf8)!)
{
  "name" : "Joe"
}

那么这不是我们想要的。 事实证明,自动生成的实现不完全适用于子类。 所以我们必须再次定制编码/解码方法。

class Person : Codable {
    var name: String?

    private enum CodingKeys : String, CodingKey {
        case name
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
    }
}

我们会为子类做同样的事情:

class Employee : Person {
    var employeeID: String?

    private enum CodingKeys : String, CodingKey {
        case employeeID = "emp_id"
    }

    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(employeeID, forKey: .employeeID)
    }
}

得到:

{
  "emp_id" : "emp123"
}

那么也不对。 我们必须转向超类实现encode(to:)。

你可能会试着调用super,并传给编码器。 有一个错误,阻止了这个在早些时候有效的代码,但雷达很快就定位到这个问题解决。 从Xcode 9 Beta 5(也许更早)开始支持:

// Employee.swift
override func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(employeeID, forKey: .employeeID)
}

结果得到

{
    "name": "Joe",
    "emp_id": "emp123"
}

请注意,这些项目被拼合入同一个容器。 Swift团队有这样的话来说,多种类型重用同一个容器:

如果需要共享容器,仍然可以调用super.encode(:encoder)和super.init(from:decoder),但是我们推荐使用更安全的容器选项。

原因是超类可以覆盖我们设定的值,且我们不知道。

相反,我们可以使用一种特殊的方法来获得已经有一个容器且准备好了编码器的超类:

try super.encode(to: container.superEncoder())

我们得到:

{
  "super" : {
    "name" : "Joe"
  },
  "emp_id" : "emp123"
}

这会在这个新的关键字“super”下生成超类的编码。 如果我们想要,我们可以自定义这个键名:

enum CodingKeys : String, CodingKey {
  case employeeID = "emp_id"
  case person
}

override func encode(to encoder: Encoder) throws {
   // ...
   try super.encode(to:
      container.superEncoder(forKey: .person))
}

其结果是:

{
  "person" : {
    "name" : "Joe"
  },
  "emp_id" : "emp123"
}

在超类中访问公共结构可以简化JSON解析,并在某些情况下减少代码重复。

用户信息UserInfo

用户信息可以在编码和解码过程中传递,如果你需要自定义数据以便在编码或解码期间改变行为或为对象提供必要的上下文。

例如,假设我们有一个为客户生成JSON遗留版本v1的API:

{
  "customer_name": "Acme, Inc",   // 旧的键名
  "migration_date": "Oct-24-1995", // 不同的日期格式?
  "created_at": "1991-05-12T12:00:00Z"
}

在这里,我们有一个与created_at字段具有不同日期格式的migration_date字段。 我们还假设name属性已经被改为只是名字。

这显然不是一个理想的情况,但现实生活中,有时你会继承一个凌乱的API。

让我们定义一个特殊的用户信息结构,它将为我们保留一些重要的值:

struct CustomerCodingOptions {
  enum ApiVersion {
      case v1
      case v2
  }
  let apiVersion = ApiVersion.v2
  let legacyDateFormatter: DateFormatter

  static let key = CodingUserInfoKey(rawValue: "com.mycompany.customercodingoptions")!
}

我们现在可以创建这个结构的实例,并将其传递给编码器或解码器:

let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy"
let options = CustomerCodingOptions(apiVersion: .v1, legacyDateFormatter: formatter)

encoder.userInfo = [ CustomerCodingOptions.key : options ]

// ...

在编码方法里面:

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    // 在这里我们可以要求这个存在...
    if let options = encoder.userInfo[CustomerCodingOptions.key] as? CustomerCodingOptions {

        // 编写正确的自定义键
        switch options.apiVersion {
        case .v1:
            try container.encode(name, forKey: .legacyCustomerName)
        case .v2:
            try container.encode(name, forKey: .name)
        }

        // 使用提供的格式化日期
        if let migrationDate = legacyMigrationDate {
            let legacyDateString = options.legacyDateFormatter.string(from: migrationDate)
            try container.encode(legacyDateString, forKey: .legacyMigrationDate)
        }

    } else {
        fatalError("We require options")
    }


    try container.encode(createdAt, forKey: .createdAt)
}

我们可以为解码初始化器完成相同的操作。

动态编码键

到目前为止,在本指南中,我们已经使用枚举来表示与Swift命名不同的代码键。 有时候这是不可能的。 考虑这种情况:

{
  "kolsh" : {
    "description" : "First only brewed in Köln, Germany, now many American brewpubs..."
  },
  "stout" : {
    "description" : "As mysterious as they look, stouts are typically dark brown to pitch black in color..."
  }
}

这是一个beer风格的列表,但实际上键是风格的名称。 我们不能用一个枚举来代表每个可能的情况,因为它可能会随着时间的推移而变化或增长。

相反,我们可以为此创建一个动态的CodingKey实现。

struct BeerStyles : Codable {
  struct BeerStyleKey : CodingKey {
    var stringValue: String
    init?(stringValue: String) {
      self.stringValue = stringValue
    }
    var intValue: Int? { return nil }
    init?(intValue: Int) { return nil }

    static let description = BeerStyleKey(stringValue: "description")!
  }

  struct BeerStyle : Codable {
    let name: String
    let description: String
  }

  let beerStyles : [BeerStyle]
}

CodingKey需要String和Int值属性和初始值设定项,但是在这种情况下,我们不需要支持整数键。 我们还为静态的“description”属性定义了一个静态键,这个键不会改变。

我们先从解码开始。

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: BeerStyleKey.self)

    var styles: [BeerStyle] = []
    for key in container.allKeys {
        let nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
            forKey: key)
        let description = try nested.decode(String.self,
            forKey: .description)
        styles.append(BeerStyle(name: key.stringValue,
            description: description))
    }

    self.beerStyles = styles
}

在这里,我们动态地遍历容器中找到的所有键,获取对容器下的引用,然后从中提取描述。

使用名称和描述我们可以手动创建一个BeeryStyle实例并将其添加到数组中。

如何编码呢?

func encode(to encoder: Encoder) throws {
    var container = try encoder.container(keyedBy: BeerStyleKey.self)
    for style in beerStyles {
        let key = BeerStyleKey(stringValue: style.name)!
        var nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
            forKey: key)
        try nested.encode(style.description, forKey: .description)
    }
}

在这里,我们遍历数组中的所有样式,为样式的名称创建一个键,并在该键上创建一个容器。 然后我们只需要将描述编码到那个容器中,我们就完成了。

正如你所看到的,创建一个自定义的CodingKey使得我们可以处理很多类型的响应。

处理错误

到目前为止,我们还没有处理任何错误。 这些是我们可能遇到的一些错误。 每个提供了一些相关的值(如DecodingError.Context,它提供了一个有用的关于什么时候错了的调试描述)。

  • DecodingError.dataCorrupted(Context) - 数据被破坏(即它看起来并不像我们所期望的那样)。 如果您提供给解码器的数据根本不是JSON,但可能是来自API失败调用的HTML错误页面,则会出现这种情况。
  • DecodingError.keyNotFound(CodingKey,Context) - 找不到必需的键。 这在问题里传递了键,上下文给出了有关这些发生的地方和原因的有用信息。 你可以捕捉到这个并为键提供一个回退值。
  • DecodingError.typeMismatch(Any.Type,Context) - 预期的一种类型,但发现另一种。 也许数据格式从一个版本的API改变到另一个版本。 你可以捕获这个错误,并尝试使用不同的类型来检索值。

编码器和解码器产生的错误在诊断问题时非常有用,使你能够灵活地动态适应某些情况并适当地处理它们。

有一个地方是迁移老版本API的响应。 比如说你编码了一个对象的版本,在某个地方存入了持久的缓存。 后来你改变了格式,但这个磁盘的数据仍然存在。 当尝试加载它时,会引发这些错误,你可以处理它们后完全迁移到新的数据格式。

进一步阅读

  • Codable.swift - 关于Swift开源的伟大之一就是我们可以看看这些东西是如何实现的。 绝对值得看看!

像视频?

如果想在屏幕录像形式中学习的那样,我把所有这些东西上制作了两个视频:

总结

这是如何使用新的Swift 4 Codable API的旋风之旅。 有什么补充? 在下面留言。

原文:Ultimate Guide to JSON Parsing With Swift 4