JSON 解析

CI

一个用于解码和编码 JSON 的库,构建于 @pointfreeco 的 Parsing 库之上。


简介

如上所述,该库是使用 Parsing 库构建的,该库为在 Swift 中编写解析代码提供了一致的方法,也就是将一些非结构化数据转换为更结构化数据的代码。 你可以通过构建解析器来实现,这些解析器是泛型的,既包含(非结构化的)输入,也包含(结构化的)输出。 真正伟大的是这些解析器可以被做成是可逆的(或双向的),这意味着它们也可以将结构化数据返回为非结构化数据,即打印

JSONParsing 库提供了预定义的解析器,专门针对输入是 JSON 的情况进行调整,为你提供了一种方便的方式来编写能够解析(解码)和打印(编码)JSON 的解析器。 与 Codable 抽象相比,这种处理 JSON 的方式有很多优点。 更多相关信息请参见 动机 章节。

快速入门

让我们看看如何使用这个库来解码和编码 JSON 数据。 想象一下,例如,你有一个描述电影的 JSON 数据

let json = """
{
  "title": "Interstellar",
  "release_year": 2014,
  "director": "Christopher Nolan",
  "stars": [
    "Matthew McConaughey",
    "Anne Hathaway",
    "Jessica Chastain"
  ],
  "poster_url": "https://www.themoviedb.org/t/p/w1280/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg",
  "added_to_favorites": true
}
""".data(using: .utf8)!

首先,我们定义一个对应的 Movie 类型

struct Movie {
  let title: String
  let releaseYear: Int
  let director: String
  let stars: [String]
  let posterUrl: URL?
  let addedToFavorites: Bool
}

然后,我们可以创建一个 JSON 解析器,来处理将 JSON 解码为这个新的数据类型

extension Movie {
  static var jsonParser: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      Field("title") { String.jsonParser() }
      Field("release_year") { Int.jsonParser() }
      Field("director") { String.jsonParser() }
      Field("stars") {
        JSONArray { String.jsonParser() }
      }
      OptionalField("poster_url") { URL.jsonParser() }
      Field("added_to_favorites") { Bool.jsonParser() }
    }
  }
}

现在,Movie.jsonParser 可以用于将 JSON 数据解码为 Movie 实例

let decodedMovie = try Movie.jsonParser.decode(json)
print(decodedMovie)
// Movie(title: "Interstellar", releaseYear: 2014, director: "Christopher Nolan", stars: ["Matthew McConaughey", "Anne Hathaway", "Jessica Chastain"], posterUrl: Optional(https://www.themoviedb.org/t/p/w1280/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg), addedToFavorites: true)

但更酷的是,完全相同的解析器,无需任何额外的工作,也可以用于将电影值编码为 JSON

let jokerMovie = Movie(
  title: "Joker",
  releaseYear: 2019,
  director: "Todd Phillips",
  stars: ["Joaquin Phoenix", "Robert De Niro"],
  posterUrl: URL(string: "https://www.themoviedb.org/t/p/w1280/udDclJoHjfjb8Ekgsd4FDteOkCU.jpg")!,
  addedToFavorites: true
)

let jokerJson = try Movie.jsonParser.encode(jokerMovie)
print(String(data: jokerJson, encoding: .utf8)!)
// {"added_to_favorites":true,"director":"Todd Phillips","poster_url":"https://www.themoviedb.org/t/p/w1280/udDclJoHjfjb8Ekgsd4FDteOkCU.jpg","release_year":2019,"stars":["Joaquin Phoenix","Robert De Niro"],"title":"Joker"}

有关构建 JSON 解析器的构建块的更多信息,请参见 JSON 解析器 章节。

动机 - 为什么不使用 Codable?

在 Swift 中处理 JSON 的默认方式是使用 Apple 自己的 Codable 框架。 虽然它是一个强大的抽象,但它确实存在一些缺点和限制。 让我们探讨其中的一些问题,看看 JSONParsing 库如何解决这些问题。

多个 JSON 表示形式

Codable 框架的一个限制是,任何给定的类型只能有一种 JSON 表示方式。 为了解决这个限制,一种常见的方法是引入包装类型,该类型包装了结果类型的值,并具有自定义的 Decodable 实现。 然后,在解码该类型时,你首先解码到包装类型,然后提取底层值。 虽然这种方法可行,但仅仅为了处理 JSON 解码而引入一个新类型是很麻烦的。 此外,无论何时你想使用特定的解码策略解码到底层类型,都需要显式地使用包装类型。

例如,让我们考虑以下表示 RGB 颜色的类型

struct RGBColor {
  let red: Int
  let green: Int
  let blue: Int
}

这个类型对应的 JSON 表示形式是什么? 可能是这样的

{
  "red": 205,
  "green": 99,
  "blue": 138
}

或者可能是

"205,99,138"

事实是,这两种表示形式都是合理的(以及许多其他可能性),并且你可能会有一个 API 端点以第一种格式返回 RGB 颜色,而另一个端点以第二种格式返回 RGB 颜色。 但是,当使用 Codable 时,你必须选择其中一种格式作为 RGBColor 类型使用的格式。 为了处理这两种变体,你必须定义两个单独的类型,例如 RGBColorWithObjectRepresentationRGBColorWithStringRepresentation,并将它们都符合 Codable,具有不同的解码/编码策略。

使用 JSONParsing 库,你可以轻松地创建两个单独的解析器,一个用于每个替代方案

extension RGBColor {
  static var jsonParserForObjectRepresentation: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      Field("red") { Int.jsonParser() }
      Field("green") { Int.jsonParser() }
      Field("blue") { Int.jsonParser() }
    }
  }

  static var jsonParserForStringRepresentation: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      JSONString {
        Int.parser()
        ","
        Int.parser()
        ","
        Int.parser()
      }
    }
  }
}

现在你可以在给定的情况下使用最适合的那个

// in one place in the app

let colorJson1 = """
{
  "red": 205,
  "green": 99,
  "blue": 138
}
""".data(using: .utf8)!
// decode
let color1 = try RGBColor.jsonParserForObjectRepresentation.decode(colorJson1)
print(color1)
// RGBColor(red: 205, green: 99, blue: 138)

// encode
let newColorJson1 = try RGBColor.jsonParserForObjectRepresentation.encode(color1)
print(String(data: newColorJson1, encoding: .utf8)!)
// {"blue":138,"green":99,"red":205}

// in another place in the app

let colorJson2 = """
"55,190,25"
""".data(using: .utf8)!
// decode
let color2 = try RGBColor.jsonParserForStringRepresentation.decode(colorJson2)
print(color2)
// RGBColor(red: 205, green: 99, blue: 138)

// encode
let newColorJson2 = try RGBColor.jsonParserForStringRepresentation.encode(color2)
print(String(data: newColorJson2, encoding: .utf8)!)
// "55,190,25"

如果你愿意,你甚至可以定义一个可配置的函数,在同一个地方处理这两种变体

extension RGBColor {
  static func jsonParser(useStringRepresentation: Bool = false) -> some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      if useStringRepresentation {
        JSONString {
          Int.parser()
          ","
          Int.parser()
          ","
          Int.parser()
        }
      } else {
        Field("red") { Int.jsonParser() }
        Field("green") { Int.jsonParser() }
        Field("blue") { Int.jsonParser() }
      }
    }
  }
}

try RGBColor.jsonParser(useStringRepresentation: false).decode(colorJson1)
// RGBColor(red: 205, green: 99, blue: 138)
try RGBColor.jsonParser(useStringRepresentation: true).decode(colorJson2)
// RGBColor(red: 205, green: 99, blue: 138)

Date 类型

也许遇到一个类型只能有一种 Codable 符合性的限制的最常见方式是在处理 Date 类型时。 事实上,它非常普遍,以至于 Codable 框架甚至提供了一种特殊的方式来管理如何解码/编码 Date 类型,通过 JSONDecoderJSONEncoder 上提供的 dateDecodingStrategy/dateEncodingStrategy 属性。 虽然这确实有效,但是对于一个特定的类型进行特殊处理有点奇怪,这看起来与你处理所有其他类型的方式截然不同。 此外,在 Encoder/Decoder 类型上进行配置意味着你不能在同一个 JSON 对象中拥有多个日期格式。

另一方面,使用 JSONParsingDate 类型不必作为例外处理。 我们在上面使用 RGBColor 类型时看到,我们可以简单地创建一个与 JSON API 中使用的所需表示形式匹配的解析器。 该库还将静态的 jsonParser(formatter:) 方法扩展到 Date 类型,这允许构建一个根据给定的 DateFormatter 解码/编码日期的 JSON 解析器

let json = """
{
  "date1": "1998-11-20",
  "date2": "2021-06-01T13:09:09Z"
}
""".data(using: .utf8)!

struct MyType {
  let date1: Date
  let date2: Date
}

let basicFormatter = DateFormatter()
basicFormatter.dateFormat = "yyyy-MM-dd"

let isoFormatter = DateFormatter()
isoFormatter.dateFormat = "yyyy-MM-dd'T'HH':'mm':'ss'Z'"

extension MyType {
  static var jsonParser: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      Field("date1") { Date.jsonParser(formatter: basicFormatter) }
      Field("date2") { Date.jsonParser(formatter: isoFormatter) }
    }
  }
}

let parsedValue = try MyType.jsonParser.decode(json)
print(parsedValue)
// MyType(date1: 1998-11-20 00:00:00 +0000, date2: 2021-06-01 13:09:09 +0000)
let encodedJson = try MyType.jsonParser.encode(parsedValue)
print(String(data: encodedJson, encoding: .utf8)!)
// {"date1":"1998-11-20","date2":"2021-06-01T13:09:09Z"}

解码和编码逻辑不同步

Codable 具有一个非常酷的功能,由于与 Swift 编译器的集成,它能够自动合成 Swift 类型的解码和编码实现。 不幸的是,在实践中,自动合成的实现通常对于你的用例来说是不正确的,因为它假设你的 JSON 数据和你的 Swift 数据类型在结构上完全匹配。 由于各种原因,这种情况通常不会发生。 首先,你可能正在处理你不拥有的 JSON API,因此可能会以不理想的格式提供数据。 但是即使你确实拥有 API 代码,它也可能被多个平台使用,这意味着你无法专门定制它以使其与你的 Swift 代码完美配合。 此外,Swift 具有一些功能,例如枚举,这些功能根本无法在 JSON 中以等效的方式表达。

因此,在实践中,当使用 Codable 时,你通常必须手动实现解码和编码逻辑。 在这种情况下,问题是它们必须单独实现。 这意味着,每当预期的 JSON 格式以任何方式更改时,你都必须记住相应地更新 init(from:)(解码)和 encode(to:)(编码)实现。

另一方面,使用 JSONParsing,你可以编写一个可以同时处理解码和编码的 JSON 解析器(如 快速入门 章节所示)。 这意味着你可以保证随着你的 JSON API 的发展,这两个转换始终保持同步。

自定义字符串解析

回想一下我们之前如何为 RGBColor 类型定义一个 JSON 解析器,其中 JSON 表示形式是一个逗号分隔的字符串。 看起来像这样

extension RGBColor {
  static var jsonParserForStringRepresentation: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      JSONString {
        Int.parser()
        ","
        Int.parser()
        ","
        Int.parser()
      }
    }
  }
}

let colorJson = """
"55,190,25"
""".data(using: .utf8)!
let color = try RGBColor.jsonParserForStringRepresentation.decode(colorJson)
print(color)
// RGBColor(red: 55, green: 190, blue: 25)
let newColorJson2 = try RGBColor.jsonParserForStringRepresentation.encode(color2)
print(String(data: newColorJson2, encoding: .utf8)!)
// "55,190,25"

在该示例中,它用于突出显示我们可以处理同一类型的不同 JSON 表示形式。 然而,它实际上也展示了该库的另一个优点,即它与 Parsing 库的集成使其非常方便地处理 JSON 表示形式需要自定义字符串转换的类型。

让我们尝试使用 Codable 完成同样的事情

extension RGBColor: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let stringValue = container.decode(String.self)
    self.red = ???
    self.green = ???
    self.blue = ???
  }
}

我们如何从解码后的字符串中获取 rgb 组件? Codable 抽象并没有真正提供一个通用的答案。 如果我们愿意,我们当然可以在这里使用 Parsing

extension RGBColor: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let stringValue = try container.decode(String.self)
    self = try Parse(Self.init) {
      Int.parser()
      ","
      Int.parser()
      ","
      Int.parser()
    }
    .parse(stringValue)
  }
}

但它并没有像在 JSONParsing 示例中那样无缝集成到代码的其余部分,迫使我们手动调用 parse 方法。 并且,同样,这只是等式的一半,我们仍然必须处理编码,必须单独实现。

具有替代表示形式的 JSON

假设你正在使用一个以以下格式提供配料列表的 API

let ingredientsJson = """
[
  {
    "name": "milk",
    "amount": {
      "value": 2,
      "unit": "dl"
    }
  },
  {
    "name": "salt",
    "amount": "a pinch"
  }
]
""".data(using: .utf8)!

如你所见,amount 可以表示为值和单位的组合,也可以表示为字符串。 在 Swift 中,这最自然地使用枚举来表示

struct Ingredient {
  enum Amount {
    case exact(value: Int, unit: String)
    case freetext(String)
  }

  let name: String
  let amount: Amount
}

在这种情况下,我们无法为 Amount 类型获得合适的合成的 Codable 符合性,因此我们别无选择,只能自己实现这些方法。 让我们进行 Decodable 符合性

extension Ingredient.Amount: Decodable {
  enum CodingKeys: CodingKey {
    case unit
    case value
  }

  init(from decoder: Decoder) throws {
    do {
      let container = try decoder.singleValueContainer()
      self = .freetext(try container.decode(String.self))
    } catch {
      let container = try decoder.container(keyedBy: CodingKeys.self)
      let value = try container.decode(Int.self, forKey: .value)
      let unit = try container.decode(String.self, forKey: .unit)
      self = .exact(value: value, unit: unit)
    }
  }
}

对于 Ingredient 类型,我们可以只使用自动合成的符合性

extension Ingredient: Decodable {}

现在我们可以使用 JSONDecoderingredientsJson 解码为 Ingredient 列表

let ingredients = try JSONDecoder().decode([Ingredient].self, from: ingredientsJson)
print(ingredients)
// [Ingredient(name: "milk", amount: Ingredient.Amount.exact(value: 2, unit: "dl")), Ingredient(name: "salt", amount: Ingredient.Amount.freetext("a pinch"))]

这样就行了。 我们确实必须创建一个显式的 CodingKeys 类型以及两个单独的 containers 来处理这两种情况,这有点额外的样板代码,但还算不错。 但实际上这里有一个更根本的问题。 要看到这一点,让我们修改 JSON 输入,如下所示

[
  ...
  {
    "name": "salt",
-   "amount": "a pinch"
+   "amount": 3
  }
]
""".data(using: .utf8)!

因此,amount 现在只是一个数字,这是不允许的。 当我们尝试解码列表时,我们会收到一个错误

do {
  let ingredients = try JSONDecoder().decode([Ingredient].self, from: ingredientsJson)
} catch {
  print(error)
  // typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1", intValue: 1), CodingKeys(stringValue: "amount", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found a number instead.", underlyingError: nil))
}

错误消息不太容易阅读,但隐藏在其中的消息是:"Expected to decode Dictionary<String, Any> but found a number instead."。 因此,从这个错误来看,似乎 amount 字段的唯一有效值类型是嵌套的 JSON 对象。 但我们知道实际上有第二个有效的选项,即字符串。 但是,由于我们在 init(from:) 中的(任意)选择是首先尝试将其解码为字符串,然后如果失败,则尝试另一个选项,因此在创建错误时丢失了此信息。 如果我们以其他顺序编写它,我们的错误消息将改为说 "Expected to decode String but found a number instead."。 无论哪种方式,我们都错过了我们有多个有效选择这一事实。

因此,让我们看看 JSONParsing 库如何处理这种情况! 让我们为它们编写 JSON 解析器,而不是让这些类型符合 Decodable

extension Ingredient.Amount {
  static var jsonParser: some JSONParserPrinter<Self> {
    OneOf {
      ParsePrint(.case(Self.exact)) {
        Field("value") { Int.jsonParser() }
        Field("unit") { String.jsonParser() }
      }

      ParsePrint(.case(Self.freetext)) {
        String.jsonParser()
      }
    }
  }
}

extension Ingredient {
  static var jsonParser: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      Field("name") { String.jsonParser() }
      Field("amount") { Amount.jsonParser }
    }
  }
}

我们使用了 Parsing 库中的 OneOf 解析器,它将运行多个解析器,直到一个成功为止,如果没人成功,它们的错误将被累积。 让我们尝试解码与之前相同的 JSON,看看打印的是什么1

do {
  let ingredients = try JSONArray { Ingredient.jsonParser }.decode(ingredientsJson)
} catch {
  print(error)
  // At [index 1]/"amount":
  // error: multiple failures occurred
  //
  // error: Expected an object (containing the key "value"), but found:
  // 3
  //
  // Expected a string, but found:
  // 3
}

正如您所见,两种可能性现在都已在打印的错误消息中提及。此外,作为一项额外好处,错误消息容易阅读。

这也让您对使用此库时打印的错误是什么样子有所了解。它们始终具有与您在上面看到的基本上相同的布局:一个描述问题出在哪里的路径,然后是什么地方出了问题的更详细的描述。全部采用易于阅读的格式。

解码/编码逻辑分散

我认为 Codable 抽象的另一个不太理想的地方是解码/编码逻辑位于两个不同的位置。部分逻辑在类型遵循这两个协议时在类型中实现,但您也可以通过用于执行解码/编码的 JSONDecoder/JSONEncoder 实例上的属性来控制某些行为。例如,JSONDecoder 类型具有一个 keyDecodingStrategy 属性,可用于控制在解码期间如何预处理 json 对象中的键,以及一个 dateDecodingStrategy 属性,可用于控制如何解码日期。

这意味着一个类型对 Decodable/Encodable 的遵循不是对该类型如何转换为/从 json 转换的完整描述。要完全控制这种情况的发生,您必须控制使用哪个 JSONDecoder/JSONEncoder 实例。

另一方面,使用 JSONParsing 时,您创建的任何 json 解析器都准确地决定了如何将类型转换为/从 json 表示形式转换。

JSONValue 类型

到目前为止,我们已经忽略了库的一个细节,这个细节对于开始使用它来说不是立即必要的,但有助于了解其内部工作原理。在我们创建 json 解析器的所有地方,我们都给它指定了 some JSONParser<T>some JSONParserPrinter<T> 类型,然后在解码或编码 json 数据时,我们分别使用了 decode(_:)encode(_:) 方法。

事实证明,JSONParser<T>JSONParserPrinter<T> 只是 Parser<JSONValue, T>ParserPrinter<JSONValue, T> 的类型别名(ParserPrinter 意味着它可以解析(解码)和打印(编码),有关更多详细信息,请参阅 文档,了解 Parsing 库)。

因此,我们实际上定义了以名为 JSONValue 的类型作为输入的解析器。这是一种从此库公开的类型,仅用作 json 的非常基本的类型化表示形式,如下所示:

public enum JSONValue: Equatable {
  case null
  case boolean(Bool)
  case integer(Int)
  case float(Double)
  case string(String)
  case array([JSONValue])
  case object([String: JSONValue])
}

因此,当我们调用解析器上的 decode(_:)encode(_:) 方法时,解码和编码分两步进行:json 数据被转换为/从 JSONValue 类型转换,然后使用 Parser.parse/ParserPrinter.print 方法将 JSONValue 类型依次转换为/从结果类型转换。

JSONValue 类型的主要用例只是充当这个中间层,以简化库附带的各种 json 解析器的实现。但是,它实际上可以单独使用。例如,您今天可能拥有这样的代码:

let json: [String: Any] = [
  "title": "hello",
  "more_info": ["a": 1, "b": 2, ...],
  ...
]
let jsonData = try JSONSerialization.data(withJSONObject: json)
var request = URLRequest(url: requestUrl)
request.httpMethod = "POST"
request.httpBody = jsonData

虽然这确实有效,但 json 的类型为 [String: Any] 这一事实意味着它实际上可能是一个包含任何类型的数据的字典。特别是,它可以包含不是有效 json 数据的数据,并且编译器不会让您知道。例如,我们可以在 title 字段中添加一个 Date,编译器会很好地处理它,但会导致运行时崩溃

let json: [String: Any] = [
  "title": Date(),
  "more_info": ["a": 1, "b": 2, ...],
  ...
]
let jsonData = try JSONSerialization.data(withJSONObject: json)
// runtime crash: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (__NSTaggedDate)'

通过在此场景中使用 JSONValue 类型,您可以获得编译时保证,确保您的 json 数据有效。并且由于 JSONValue 符合许多 ExpressibleBy... 协议,因此实际上可以使用与以前完全相同的语法对其进行初始化。因此,前面的示例变为:

let json: JSONValue = [
  "title": "hello",
  "more_info": ["a": 1, "b": 2, ...],
  ...
]
let jsonData = try json.toJsonData()
// ... the rest is the same

如果我们现在尝试像以前一样用 Date() 替换 "hello",这次编译器不会让我们这样做

let json: [String: Any] = [
  "title": Date(), // compiler error: Cannot convert value of type 'Date' to expected dictionary value type 'JSONValue'
  "more_info": ["a": 1, "b": 2, ...],
  ...
]

JSON 解析器

该库附带了许多 json 解析器,可以组合在一起以处理更复杂的 json 结构。如上一节所述,它们都采用自定义类型 JSONValue 的值作为输入,因此在使用 parse/print 方法时,它们会转换为/从该类型转换。

当您想使用它们来解码/编码 json 数据(这可能是最常见的用例)时,您只需使用它们上定义的 decode/encode 方法,这些方法会为您完成与数据的转换。

Null(空值)

Null 解析器用于解析特殊的 json 值 null。当您需要显式确保某个值为空时,可以使用它。

let nullJson: JSONValue = .null
let nonNullJson: JSONValue = 5.0

try Null().parse(nullJson)
// ()
try Null().parse(nonNullJson)
// throws:
// Expected a null value, but found:
// 5.0

当用作打印机(编码器)时,Null 解析器打印 .null

try Null().print(()) // .null

JSONBoolean(JSON 布尔值)

JSONBoolean 解析器用于解析 json 布尔值。仅当给定 falsetrue json 值时才成功,并返回相应的 Bool 值。

let booleanJson: JSONValue = false
let nonBooleanJson: JSONValue = [
  "key1": 1,
  "key2": "hello"
]

try JSONBoolean().parse(booleanJson)
// false
try JSONBoolean().parse(nonBooleanJson)
// throws:
// Expected a boolean, but found:
// {
//   "key1": 1,
//   "key2": "hello"
// }

构造 JSONBoolean 解析器的另一种方法是通过 Bool 类型上的静态 jsonParser() 方法

try Bool.jsonParser().parse(booleanJson)
// false

JSONBoolean 解析器还可以用于打印(编码)回 json

try Bool.jsonParser().print(true)
// .boolean(true)

JSONNumber(JSON 数字)

JSONNumber 解析器用于解析 json 数字。值得注意的是,JSONValue 类型在浮点数和整数之间进行了区分。当使用它解析为浮点类型时,解析器采用一个名为 allowInteger 的可选参数,该参数控制它是否也对整数以及浮点数成功。如果未指定,则默认为 true

let integerJson: JSONValue = 10 // or .integer(10)
let floatJson: JSONValue = 2.4 // or .float(2.4)
let nonNumberJson: JSONValue = "hello"

try JSONNumber<Int>().parse(integerJson)
// 10
try JSONNumber<Double>().parse(floatJson)
// 2.4
try JSONNumber<Int>().parse(floatJson)
// throws:
// Expected an integer number, but found:
// 2.4
try JSONNumber<Double>().parse(integerJson)
// 10.0
try JSONNumber<Double>(allowInteger: false).parse(integerJson)
// throws:
// Expected a floating point number, but found:
// 10
try JSONNumber<Double>().parse(nonNumberJson)
// throws:
// Expected a number, but found:
// "hello"

或者,可以通过 BinaryIntegerBinaryFloatingPoint 上定义的 jsonParser() 静态方法构造 JSONNumber 解析器

try Int.jsonParser().parse(integerJson) // 10
try Int64.jsonParser().parse(integerJson) // 10
try Double.jsonParser().parse(floatJson) // 2.4
try CGFloat.jsonParser(allowInteger: false).parse(floatJson) // 2.4

注意:当解码 json 数据时,使用 decode 方法,如果 json 对象中的数字具有任何小数(包括仅 0),则将其解释为浮点数。

let json = """
{
  "a": 10,
  "b": 10.5,
  "c": 10.0
}
""".data(using: .utf8)!

try Field("a") { Int.jsonParser() }.decode(json)
// 10
try Field("b") { Int.jsonParser() }.decode(json)
// throws:
// At "b":
// Expected an integer number, but found:
// 10.5
try Field("c") { Int.jsonParser() }.decode(json)
// throws:
// At "c":
// Expected an integer number, but found:
// 10.0
try Field("b") { Double.jsonParser() }.decode(json)
// 10.5
try Field("c") { CGFloat.jsonParser() }.decode(json)
// 10.0

JSONNumber 解析器还可以用于打印到 json

try Int.jsonParser().print(25) // .integer(25)
try Double.jsonParser().print(1.6) // .float(1.6)

JSONString(JSON 字符串)

JSONString 解析器用于解析 json 字符串。正如之前的章节中所示,它还可以给定一个字符串解析器,用于执行字符串值的自定义解析。

let stringJson: JSONValue = "120,200,43"
let nonStringJson: JSONValue = [1, 2, 3]

try JSONString().parse(stringJson)
// "120,200,43"
try JSONString().parse(nonStringJson)
// throws:
// Expected a string, but found:
// [ 1, 2, 3 ]
try JSONString {
  Int.parser()
  ","
  Int.parser()
  ","
  Int.parser()
}.parse(stringJson)
// (120, 200, 43)

let nonMatchingStringJson: JSONValue = "apple"

try JSONString {
  Int.parser()
  ","
  Int.parser()
  ","
  Int.parser()
}.parse(stringJson)
// throws:
// error: unexpected input
//  --> input:1:1
// 1 | apple
//   | ^ expected integer

还有一个初始化程序的版本接受字符串转换。转换是 Parsing 库中引入的概念,其工作方式类似于双向函数。该库还公开了许多预定义的转换,例如 representing(_:) 转换,可用于在 RawRepresentable 类型及其原始值之间进行转换。将其与 JSONString 解析器一起使用如下所示

enum Direction: String {
  case up, down, left, right
}
extension Direction {
  static let jsonParser = JSONString(.representing(Direction.self))
}
let json: JSONValue = "left"
let direction = Direction.jsonParser.parse(json)
print(direction) // Direction.left

try Direction.jsonParser.print(direction)
// .string("left")

当您不需要任何自定义解析,而只想按原样解析 json 字符串时,您还可以选择使用 String 类型上定义的静态 jsonParser() 方法定义解析器

let json: JSONValue = "hello"
try String.jsonParser().parse(json)
// "hello"

JSONString 可以用作打印机,以打印(解码)到 json,只要提供给它的底层字符串解析器本身是一个打印机即可。

try JSONString {
  Int.parser()
  ","
  Int.parser()
  ","
  Int.parser()
}.print((120, 200, 43))
// .string("120,200,43")

JSONArray(JSON 数组)

JSONArray 解析器用于解析 json 数组。您通过提供应应用于数组的每个元素的解析器来构造它。作为一项额外好处,您还可以选择指定数组必须具有特定大小,方法是给它一个范围或一个数字。它看起来像这样用于解析 json

let directionArrayJson: JSONValue = ["left", "left", "right", "up"]
let numberArrayJson: JSONValue = [1, 2, 3]
let nonArrayJson: JSONValue = 10.5

try JSONArray {
  Direction.jsonParser
}.parse(directionArrayJson)
// [Direction.left, Direction.left, Direction.right, Direction.up]

try JSONArray(1...3) {
  Direction.jsonParser
}.parse(directionArrayJson)
// throws:
// Expected 1-3 elements in array, but found 4.

try JSONArray(3) {
  Direction.jsonParser
}.parse(directionArrayJson)
// throws:
// Expected 3 elements in array, but found 4.

try JSONArray {
  Direction.jsonParser
}.parse(numberArrayJson)
// throws:
// At [index 0]:
// Expected a string, but found:
// 1

try JSONArray {
  Int.jsonParser()
}.parse(numberArrayJson)
// [1, 2, 3]

try JSONArray {
  Int.jsonParser()
}.parse(nonArrayJson)
// throws:
// Expected an array, but found:
// 10.5

对于打印(只要给定它的元素解析器具有打印功能即可)

try JSONArray {
  Direction.jsonParser
}.print([Direction.right, .left, .down])
// .array(["right", "left", "down"])

JSONObject(JSON 对象)

JSONObject 解析器用于将 json 对象解析为字典。在其最基本的形式中,它采用单个 Value 解析器,应用于 json 对象中的每个值。解析后的结果将是一个 [String: Value.Output] 字典,其中 Value.Output 是从 Value 解析器返回的类型。

let objectJson: JSONValue = .object([
  "url1": "https://www.example.com/1",
  "url2": "https://www.example.com/2",
  "url3": "https://www.example.com/3",
])

let dictionary = try JSONObject {
  URL.jsonParser()
}.parse(objectJson)
print(dictionary)
// ["url1": https://www.example.com/1, "url3": https://www.example.com/3, "url2": https://www.example.com/2]

try JSONObject {
  URL.jsonParser()
}.print(dictionary)
// .object(["url1": "https://www.example.com/1", "url3": "https://www.example.com/3", "url2": "https://www.example.com/2"])

但是您也可以通过添加一个 keys 解析器参数来指定的自定义解析为任何 Hashable 类型

let objectJson: JSONValue = [
  "key_1": "Steve Jobs",
  "key_2": "Tim Cook"
]

let dictionary = try JSONObject {
  String.jsonParser()
} keys: {
  "key_"
  Int.parser()
}.parse(objectJson)
print(dictionary)
// [1: "Steve Jobs", 2: "Tim Cook"]

try JSONObject {
  String.jsonParser()
} keys: {
  "key_"
  Int.parser()
}.print(dictionary)
// .object(["key_1": "Steve Jobs", "key_2": "Tim Cook"])

或者通过将字符串转换传递给初始化程序,例如 representing 转换,将键转换为某个 RawRepresentable 类型

struct UserID: RawRepresentable, Hashable {
  var rawValue: String
}
let usersJson: JSONValue = .object([
  "abc": "user 1",
  "def": "user 2",
])
let dictionary = try JSONObject(keys: .representing(UserID.self)) {
  String.jsonParser()
}.parse(usersJson)
print(dictionary)
// [UserID(rawValue: "abc"): "user 1", UserID(rawValue: "def"): "user 2"]

try JSONObject(keys: .representing(UserID.self)) {
  String.jsonParser()
}.print(dictionary)
// .object(["abc": "user 1", "def": "user 2"])

就像 JSONArray 解析器一样,它可以被限制为仅接受特定数量的元素(键/值对)。

let emptyObjectJson: JSONValue = [:]
try JSONObject(1...) {
  URL.jsonParser()
}.parse(emptyObjectJson)
// throws: Expected at least 1 key/value pair in object, but found 0.

let emptyDictionary: [String: URL] = [:]
try JSONObject(1...) {
  URL.jsonParser()
}.print(emptyDictionary)
// throws: An JSONObject parser requiring at least 1 key/value pair was given 0 to print.

Field(字段)

Field 解析器用于解析给定字段处的单个值。它将键作为 String 和要应用于在该键处找到的值的 json 解析器作为输入。

let personJson: JSONValue = [
  "first_name": "Steve",
  "last_name": "Jobs",
  "age": 56,
]
let personJsonWithoutFirstName: JSONValue = [
  "last_name": "Cook",
  "age": 62,
]

try Field("first_name") {
  String.jsonParser()
}.parse(personJson)
// "Steve"

try Field("first_name") {
  String.jsonParser()
}.print("Steve")
// .object(["first_name": "Steve"])

try Field("first_name") {
  Int.jsonParser()
}.parse(personJson)
// throws:
// At "first_name":
// Expected an integer number, but found:
// "Steve"

try Field("first_name") {
  String.jsonParser()
}.parse(personJsonWithoutFirstName)
// throws:
// Key "first_name" not present.

大多数时候,您可能希望将多个 Field 解析器组合在一起,以解析为更复杂的结果类型。对于上面的示例,您可能会有一个 Person 类型,您想将 json 转换为该类型。为此,我们可以利用 Parsing 库公开的 memberwise 转换。

struct Person {
  let firstName: String
  let lastName: String
  let age: Int
}

extension Person {
  static var jsonParser: some JSONParserPrinter<Self> {
    try ParsePrint(.memberwise(Person.init)) {
      Field("first_name") { String.jsonParser() }
      Field("last_name") { String.jsonParser() }
      Field("age") { Int.jsonParser() }
    }
  }
}

let person = try Person.jsonParser.parse(personJson)
// Person(firstName: "Steve", lastName: "Jobs", age: 56)

try Person.jsonParser.print(person)
// .object(["first_name": "Steve", "last_name": "Jobs", "age": 56])

OptionalField(可选字段)

OptionalField 解析器的工作方式类似于 Field 解析器,但它允许该字段不存在(或为 null)。要了解它的用途,让我们用一个名为 salary 的新字段扩展 Person 类型

struct Person {
  let firstName: String
  let lastName: String
  let age: Int
+ let salary: Double?
}

然后我们可以按以下方式扩展 Person.jsonParser

try ParsePrint(.memberwise(Person.init)) {
  Field("first_name") { String.jsonParser() }
  Field("last_name") { String.jsonParser() }
  Field("age") { Int.jsonParser() }
+ OptionalField("salary") { Double.jsonParser() }
}

现在它可以处理带有或不带有薪水的 person json 值。

let personJsonWithSalary: JSONValue = [
  "first_name": "Bob",
  "last_name": "Bobson",
  "age": 50,
  "salary": 12000
]
let personJsonWithoutSalary: JSONValue = [
  "first_name": "Mark",
  "last_name": "Markson",
  "age": 20
]

let person1 = try Person.jsonParser.parse(personJsonWithSalary)
// Person(firstName: "Bob", lastName: "Bobson", age: 50, salary: 12000.0)
try Person.jsonParser.print(person1)
// .object(["first_name": "Bob", "last_name": "Bobson", "age": 50, "salary": 12000.0])

let person2 = try Person.jsonParser.parse(personJsonWithoutSalary)
// Person(firstName: "Mark", lastName: "Markson", age: 20, salary: nil)
try Person.jsonParser.print(person2)
// .object(["first_name": "Mark", "last_name": "Markson", "age": 20])

您可以选择提供一个 default 值,而不是将缺少的值视为 nil,以用作后备

struct Person {
  let firstName: String
  let lastName: String
  let age: Int
- let salary: Double?
+ let salary: Double
}

extension Person {
  static var jsonParser: some JSONParserPrinter<Self> {
    try ParsePrint(.memberwise(Person.init)) {
      Field("first_name") { String.jsonParser() }
      Field("last_name") { String.jsonParser() }
      Field("age") { Int.jsonParser() }
-     OptionalField("salary") { Double.jsonParser() }
+     OptionalField("salary", default: 0) { Double.jsonParser() }
    }
  }
}

现在,解析没有薪水的 person json 将使用默认值 0

let person = try Person.jsonParser.parse(personJsonWithoutSalary)
// Person(firstName: "Mark", lastName: "Markson", age: 20, salary: 0)
try Person.jsonParser.print(person)
// .object(["first_name": "Mark", "last_name": "Markson", "age": 20])

与 Codable 集成

虽然此库旨在能够作为 Codable 的功能齐全的替代品独立存在,但它确实附带了一些工具来帮助桥接这两个世界,允许它们混合在一起。这很重要,部分原因是您可能正在使用其他库,这些库迫使您在某些地方使用 Codable,部分原因是它允许您一次一个模型地转换使用 Codable 的代码库。让我们来看看它是如何工作的。

Codable 集成到 JSONParsing 代码中

假设您有以下类型

struct Person {
  let name: String
  let age: Int
  let favoriteMovie: Movie?
}

假设 Movie 类型遵循 Codable 协议,并且你想为 Person 创建一个 JSON 解析器。对于这种情况,该库为所有遵循 Decodable 协议的类型扩展了一个 jsonParser(decoder:) 方法,该方法接受一个可选的 JSONDecoder 参数。如果类型也遵循 Encodable 协议,该方法还会接受一个可选的 JSONEncoder 参数。 因此,在我们的示例中,我们可以利用此方法在解析实现中处理 Movie 类型。

extension Person {
  static var jsonParser: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      Field("name") { String.jsonParser() }
      Field("age") { Int.jsonParser() }
      Field("favorite_movie") { Movie.jsonParser() }
    }
  }
}

如果我们需要自定义 Movie 类型的解码/编码,我们可以像这样传递一个自定义的 decoder 和/或 encoder。

let jsonDecoder: JSONDecoder = ...
let jsonEncoder: JSONEncoder = ...
extension Person {
  static var jsonParser: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      ...
      Field("favoriteMovie") { Movie.jsonParser(decoder: jsonDecoder, encoder: jsonEncoder) }
    }
  }
}

JSONParsing 集成到 Codable 代码中

这就是与 Codable 集成的一个方面。但是反过来呢?如果我们确实有一个能够解码 Movie 的 JSON 解析器,并且我们使用 Codable 来处理 Person 类型呢? 对于这种情况,该库提供了 decoding/encoding 容器上各种方法的重载,这些重载接受 JSON 解析器作为输入。 让我们看看如何使用它,通过使 Person 类型同时遵循 DecodableEncodable 协议。

extension Person: Decodable {
  enum CodingKeys: String, CodingKey {
    case name
    case age
    case favoriteMovie = "favorite_movie"
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)
    self.age = try container.decode(Int.self, forKey: .age)
    self.favoriteMovie = try container.decodeIfPresent(forKey: .favoriteMovie) {
      Movie.jsonParser
    }
  }
}

extension Person: Encodable {
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.name, forKey: .name)
    try container.encode(self.age, forKey: .age)
    try container.encodeIfPresent(self.favoriteMovie, forKey: .favoriteMovie) {
      Movie.jsonParser
    }
  }
}

在这里,我们使用了 KeyedDecodingContainer.decodeIfPresentKeyedEncodingContainer.encodeIfPresent 方法的重载,这些重载接受 JSON 解析器作为输入。 除了接受额外的 JSON 解析器参数之外,解码重载还使 type 参数成为可选的,因为它总是可以被推断出来。 但是如果你想,你仍然可以像默认版本一样显式地指定它们。

extension Person: Decodable {
  ...
  init(from decoder: Decoder) throws {
    ...
-   self.favoriteMovie = try container.decodeIfPresent(forKey: .favoriteMovie) {
+   self.favoriteMovie = try container.decodeIfPresent(Movie.self, forKey: .favoriteMovie) {
      Movie.jsonParser
    }
  }
}

基准测试

该库提供了一些基准测试,比较了使用该库进行解码和编码与相应的 Codable 实现的执行时间。

MacBook Pro (14-inch, 2021)
Apple M1 Pro (10 cores, 8 performance and 2 efficiency)
16 GB (LPDDR5)

name                                     time           std        iterations
-----------------------------------------------------------------------------
Decoding.JSONDecoder (Codable)            174917.000 ns ±   3.19 %       7610
Decoding.JSONParser                       169625.000 ns ±   2.20 %       8070
Decoding.JSONParser (mixed with Codable)  311250.000 ns ±   8.36 %       4467
Decoding.JSONParser (from JSONValue)       67042.000 ns ±   2.06 %      20820
Encoding.JSONEncoder (Codable)           1212416.500 ns ±   0.96 %       1144
Encoding.JSONParser                      2082541.000 ns ±  22.11 %        680
Encoding.JSONParser (mixed with Codable) 2889500.000 ns ±  23.28 %        465
Encoding.JSONParser (to JSONValue)        397417.000 ns ±   1.09 %       3499

安装

你可以使用 SPM 通过将以下内容添加到 Package.swift 文件中来添加该库作为依赖项

dependencies: [
  .package(url: "https://github.com/oskarek/swift-json-parsing", from: "0.2.0"),
]

然后在每个需要访问它的模块中

.target(
  name: "MyModule",
  dependencies: [
    .product(name: "JSONParsing", package: "swift-json-parsing"),
  ]
),

许可证

该库是在 MIT 许可证下发布的。 有关详细信息,请参见 LICENSE

脚注

  1. 在撰写本文时,这实际上是一个小谎。 在这种确切的情况下,第一行 At [index 1]/"amount": 实际上会被分成两行,分别为 At [index 1]:error: At "amount":。 这是由于当前存在一个限制,导致错误路径无法以理想的方式打印,希望在不久的将来能够修复。 但是在许多其他情况下,错误路径将以简洁的格式打印,所以我仍然想展示该版本。