Swift DynamicJSON

IDE: Xcode 15 Package managers: SwiftPM, Carthage License: Apache

DynamicJSON 是一个用于表示、查询和操作通用 JSON 值的框架。该框架提供

目录
1.  表示 JSON 数据
2.  访问 JSON 值
   2.1  JSON 位置
   2.2  JSON Pointer
3.  使用 JSON Path 查询
4.  修改 JSON 值
   4.1  修改 API
   4.2  JSON Patch
5.  合并 JSON 值
   5.1  对称合并
   5.2  覆盖合并
   5.3  JSON Merge Patch
6.  验证 JSON 数据
   6.1  实现概述
   6.2  验证 API
   6.3  元数据和默认值

 

表示 JSON 数据

DynamicJSON 框架中,所有 JSON 值都使用枚举 JSON 表示。枚举 JSON 定义了以下情况

indirect enum JSON: Hashable, Codable, CustomStringConvertible, ... {
  case null
  case boolean(Bool)
  case integer(Int64)
  case float(Double)
  case string(String)
  case array([JSON])
  case object([String : JSON])
  ...
}

JSON 值可以使用 Swift 字面量语法轻松构建。这是一个初始化小型基于 JSON 的数据结构的示例

let json0: JSON = [
  "foo": true,
  "str": "one two",
  "object": [
    "value": nil,
    "arr": [1, 2, 3],
    "obj": [ "x" : 17.6 ]
  ]
]

还有 初始化器,用于将 StringData 对象形式的 JSON 编码数据转换为 JSON 枚举。以下代码从字符串字面量中的 JSON 编码值初始化 JSON 值。

let json1 = try JSON(string: """
  {
    "foo": true,
    "str": "one two",
    "object": {
      "value": null,
      "arr": [1, 2, 3],
      "obj": { "x" : 17.6 }
    }
  }
""")

任何可编码类型都可以使用初始化器 init(encodable:) 转换为 JSON 值。或者,可以使用 init()。这是最通用的初始化器,也可以将基本类型(如 BoolIntString 等)强制转换为 JSON。

struct Person: Codable {
  let name: String
  let age: Int
  let children: [Person]
}
let person = Person(name: "John", age: 34,
                    children: [ Person(name: "Sofia", age: 5, children: []) ])
let json2 = try JSON(encodable: person)
print(json2.description)

执行此代码将打印 Person 的以下基于 JSON 的表示

{
  "age" : 34,
  "children" : [
    {
      "age" : 5,
      "children" : [],
      "name" : "Sofia"
    }
  ],
  "name" : "John"
}

也可以反过来,通过方法 coerce() 将基于 JSON 的表示转换为强类型数据结构。

let json3: JSON = [
  "name": "Matthew",
  "age": 29,
  "children": []
]
let person2: Person = try json3.coerce()

访问 JSON 值

可以使用动态成员查找来识别和访问较大 JSON 文档中的 JSON 值,就像数据是完全结构化的(例如,通过将其表示为结构体)。以下是一些示例,展示了如何以不同方式访问 json1object 中数组 arr 的第一个元素。所有表达式都返回 JSON 值 1。

DynamicJSON 中,JSON 值的组件通过协议 JSONReferenceSegmentableJSONReference 的实现来标识。以下代码展示了实现 JSON 引用的核心方法

protocol JSONReference: CustomStringConvertible {
  // Returns a new JSONReference with the given member selected.
  func select(member: String) -> Self
  // Returns a new JSONReference with the given index selected.
  func select(index: Int) -> Self
  // Retrieve value at which this reference is pointing from JSON document `value`.
  func get(from value: JSON) -> JSON?
  // Replace value at which this reference is pointing with `json` within `value`.
  func set(to json: JSON, in value: JSON) throws -> JSON
  // Mutate value at which this reference is pointing within JSON document `value`
  // with function `proc`.
  func mutate(_ json: inout JSON, with proc: (inout JSON) throws -> Void) throws
}

protocol SegmentableJSONReference: JSONReference {
  associatedtype Segment: JSONReferenceSegment
  // An array of segments representing the reference.
  var segments: [Segment] { get }
  // Creates a new `SegmentableJSONReference` on top of this reference.
  func select(segment: Segment) -> Self
  // Decomposes this reference into the top segment selector and its parent.
  var deselect: (Self, Segment)? { get }
}

DynamicJSON 当前提供了 SegmentableJSONReference 的两个实现:JSONPointerJSONLocation,这是一种等效于单数 JSON Path 查询的抽象。

JSON 位置

JSONLocation 是用于在 JSON 文档中标识 JSON 值的默认实现。它基于 JSON Path 中标识值的方式,并使用 JSON Path 查询语法的受限形式。

JSONLocation 值是根据成员名称和数组索引的序列定义的,这些序列用于在 JSON 文档的结构中导航。JSONLocation 引用最多引用 JSON 文档中的一个值。以下代码总结了 JSONLocation 值的表示方式

indirect enum JSONLocation: SegmentableJSONReference, Codable, Hashable, CustomStringConvertible {
  case root
  case member(JSONLocation, String)
  case index(JSONLocation, Int)
  
  enum Segment: JSONReferenceSegment, Codable, Hashable, CustomStringConvertible {
    case member(String)
    case index(Int)
    ...
  }
  ...
}

JSON 位置是 JSON 结构中元素的路径。路径的每个元素都称为。JSON 位置语法支持两种不同的形式来表示此类段序列。每个序列都以 $ 开头,表示 JSON 文档的“根”。表示段序列最常见的形式是使用点表示法

$.store.book[0].title

虽然访问数组索引始终使用方括号表示法完成,但也可以使用方括号表示法来表示对象成员的访问

$['store']['book'][0]['title']

也可以混合使用点和方括号表示法。点仅在属性名称之前使用,并且永远不会与方括号一起使用

$['store'].book[-1].title

前面的示例还展示了负索引的用法,负索引被解释为从数组末尾的偏移量,其中 -1 指的是最后一个元素。

JSONLocation API 支持多个初始化器,用于创建 JSON 位置引用

let r1 = try JSONLocation("$['store']['book'][0]['title']")
let r2 = JSONLocation(segments: [.member("store"),
                                 .member("book"),
                                 .index(0),
                                 .member("title")])

JSONLocation 定义了以下常用方法

indirect enum JSONLocation: SegmentableJSONReference, ... {
  // The segments defining this `JSONLocation`.
  var segments: [Segment]
  // Returns a new JSONLocation with the given member selected.
  func select(member: String) -> JSONLocation
  // Returns a new JSONLocation with the given index selected.
  func select(index: Int) -> JSONLocation
  // Returns a new JSONLocation by appending the given segment.
  func select(segment: Segment) -> JSONLocation
  // Returns a matching `JSONPointer` reference if possible.
  var pointer: JSONPointer?
  // Returns a matching `JSONPath` query.
  var path: JSONPath
  // Retrieve value at this location within `value`.
  func get(from value: JSON) -> JSON?
  // Replace value at this location within `in` with `value`.
  func set(to json: JSON, in value: JSON) throws -> JSON
  // Mutate value at this location within `value` with function `proc`.
  // `proc` is provided a reference, enabling efficient in-place mutations.
  func mutate(_ json: inout JSON, with proc: (inout JSON) throws -> Void) throws
}

JSON Pointer

JSON PointerRFC 6901 规范定义,并且通常是用于引用 JSON 文档中 JSON 值的最成熟的形式体系。JSON Pointer 旨在易于在 JSON 字符串值以及统一资源标识符 (URI) 片段标识符中表达(请参阅 RFC 3986)。

与 JSON Location 类似,每个 JSON Pointer 都指定 JSON 结构中元素的路径,从其根开始。路径的每个元素要么引用对象成员,要么引用数组索引。在语法上,每个路径元素都以 "/" 为前缀。JSON Pointer 使用 "~1" 来编码成员名称中的 "/",使用 "~0" 来编码 "~"。空字符串引用 JSON 文档的根。这是一个例子

/store/book/0/title

JSON Pointer 既不支持强制元素(例如 "/0")引用数组索引,也不允许使用负索引(作为从数组末尾的偏移量)。所有数字路径元素(例如上面的 "0")要么可以匹配数组并选择索引 0,要么可以匹配对象成员 "0"。因此,没有通用的方法将 JSON Location 映射到 JSON Pointer,反之亦然。

结构体 JSONPointer 以以下方式实现 JSON Pointer 标准

struct JSONPointer: SegmentableJSONReference, Codable, Hashable, CustomStringConvertible {
  let segments: [ReferenceToken]
  
  enum ReferenceToken: JSONReferenceSegment, Hashable, CustomStringConvertible {
    case member(String)
    case index(String, Int?)
    ...
  }
  ...
}

JSONPointer API 支持多个初始化器,用于创建 JSON Pointer 引用

let p1 = try JSONPointer("/store/book/0/title")
let p2 = JSONPointer(components: ["store", "book", "0", "title"])

JSONPointer 定义了以下常用方法

struct JSONPointer: SegmentableJSONReference, ... {
  // Returns this JSONPointer as an array of reference tokens.
  var segments: [ReferenceToken]
  // Returns a new JSONPointer with the given member selected.
  func select(member: String) -> JSONPointer
  // Returns a new JSONPointer with the given index selected.
  func select(index: Int) -> JSONPointer
  // Constructs a new JSONPointer by appending the given segment to this pointer.
  func select(segment: ReferenceToken) -> JSONPointer
  // Decomposes this JSONPointer into a parent pointer and a selector reference token.
  var deselect: (JSONPointer, ReferenceToken)?
  // The reference tokens defining this `JSONPointer` value.
  var components: [String]
  // Returns all JSON locations corresponding to this `JSONPointer`.
  func locations() -> [JSONLocation]
  // Retrieve value at which this reference is pointing from JSON document `value`.
  func get(from value: JSON) -> JSON?
  // Replace value at which this reference is pointing with `json` within `value`.
  func set(to json: JSON, in value: JSON) throws -> JSON
  // Mutate value at this location within `value` with function `proc`. `proc`
  // is provided a reference, enabling efficient, in-place mutations.
  func mutate(_ json: inout JSON, with proc: (inout JSON) throws -> Void) throws
}

使用 JSON Path 查询

DynamicJSON 支持完整的 JSON Path 标准,由 RFC 9535 定义。枚举 JSONPath 表示 JSON Path 查询。JSON 提供了 query() 方法,用于将 JSON Path 查询应用于 JSON 值。

为了说明 JSON Path 查询的用法,定义了以下 JSON 值(这是来自 RFC 9535 的示例)

let jval = try JSON(string: """
  { "store": {
      "book": [
        { "category": "reference",
          "author": "Nigel Rees",
          "title": "Sayings of the Century",
          "price": 8.95 },
        { "category": "fiction",
          "author": "Evelyn Waugh",
          "title": "Sword of Honour",
          "price": 12.99 },
        { "category": "fiction",
          "author": "Herman Melville",
          "title": "Moby Dick",
          "isbn": "0-553-21311-3",
          "price": 8.99 },
        { "category": "fiction",
          "author": "J. R. R. Tolkien",
          "title": "The Lord of the Rings",
          "isbn": "0-395-19395-8",
          "price": 22.99 }
      ],
      "bicycle": {
        "color": "red",
        "price": 399
      }
    }
  }
  """)

现在可以使用 JSONPath(query:strict:) 初始化器来定义 JSON Path 查询 $.store.book[?@.price < 10].title。最后,可以通过调用其 query() 方法将路径应用于 jval。结果是一个 LocatedJSON 值数组,这些值与 jval 中的查询匹配。LocatedJSON 将找到值的位置与该位置的值组合到一个对象中。

let path = try JSONPath(query: "$.store.book[?@.price < 10].title")
var results = try value.query(path)
for result in results {
  print(result)
}

这是从此代码生成的输出。它为 jval 中与查询 path 匹配的两个值打印了两个 LocatedJSON 对象。

$['store']['book'][0]['title'] => "Sayings of the Century"
$['store']['book'][2]['title'] => "Moby Dick"

如果只需要位置或只需要值作为评估 JSON Path 查询的结果,则可以使用 JSONquery(locations:)query(values:) 方法。

上述 API 支持默认的 JSON Path 查询语言。JSON Path 具有内置的可扩展性机制,允许添加自定义函数,这些函数适用于查询过滤器。这可以通过扩展类 JSONPathEnvironment 并覆盖方法 initialize() 来实现。然后,可以将这种扩展的环境传递给结构体 JSONPathEvaluator 的初始化器,该结构体提供了一种使用扩展环境执行查询的方法。

修改 JSON 值

修改 API

DynamicJSON 使用值类型 JSON 表示 JSON 数据。有许多方法可以修改此类数据,而无需创建副本。这些方法在下面的代码片段中列出。

enum JSON: Hashable, ... {
  // Mutates this JSON value if it represents either an array or a string by
  // appending the given JSON value `json`. For arrays, `json` is appended as a
  // new element. For strings it is expected that `json` also refers to a string
  // and `json` gets appended as a string. For all other types of JSON values,
  // an error is thrown.
  mutating func append(_ json: JSON) throws
  
  // Mutates this JSON value if it represents either an array or a string by
  // inserting the given JSON value `json`. For arrays, `json` is inserted as a
  // new element at `index`. For strings it is expected that `json` also refers to
  // a string and `json` gets inserted into this string at position `index`. For
  // all other types of JSON values, an error is thrown.
  mutating func insert(_ json: JSON, at index: Int) throws
  
  // Adds a new key/value mapping or updates an existing key/value mapping in this
  // JSON object. If this JSON value is not an object, an error is thrown.
  mutating func assign(_ member: String, to json: JSON) throws
  
  // Replaces the value the location reference `ref` is referring to with `json`.
  // The replacement is done in place, i.e. it mutates this JSON value. `ref` can
  // be implemented by any abstraction implementing the `JSONReference` procotol.
  mutating func update(_ ref: JSONReference, with json: JSON) throws
  
  // Replaces the value the location reference string `ref` is referring to with
  // `json`. The replacement is done in place, i.e. it mutates this JSON value.
  // `ref` is a string representation of either `JSONLocation` or `JSONPointer`
  // references.
  mutating func update(_ ref: String, with json: JSON) throws
  
  // Mutates the JSON value the reference `ref` is referring to with function
  // `proc`. `proc` receives a reference to the JSON value, allowing efficient in
  // place mutations without automatically doing any copying. `ref` can be
  // implemented by any abstraction implementing the `JSONReference` procotol.
  mutating func mutate(_ ref: JSONReference,
                       with proc: (inout JSON) throws -> Void) throws
  
  // Mutates the JSON value the reference `ref` is referring to with function
  // `arrProc` if the value is an array or `objProc` if the value is an object. For
  // all other cases, an error is thrown. This method allows for efficient in place
  // mutations without automatically doing any copying. `ref` can be implemented by
  // any abstraction implementing the `JSONReference` procotol.
  mutating func mutate(_ ref: JSONReference,
                       array arrProc: ((inout [JSON]) throws -> Void)? = nil,
                       object objProc: ((inout [String : JSON]) throws -> Void)? = nil,
                       other proc: ((inout JSON) throws -> Void)? = nil) throws
  
  // Mutates the JSON value the reference string `ref` is referring to with function
  // `proc`. `proc` receives a reference to the JSON value, allowing efficient in
  // place mutations without automatically doing any copying. `ref` is a string
  // representation of either `JSONLocation` or `JSONPointer` references.
  mutating func mutate(_ ref: String, with proc: (inout JSON) throws -> Void) throws
  
  // Mutates the JSON array the reference string `ref` is referring to with function
  // `arrProc` if the value is an array or `objProc` if the value is an object. For
  // all other cases, an error is thrown. This method allows for efficient in place
  // mutations without automatically doing any copying. `ref` is a string
  // representation of either `JSONLocation` or `JSONPointer` references.
  mutating func mutate(_ ref: String,
                       array arrProc: ((inout [JSON]) throws -> Void)? = nil,
                       object objProc: ((inout [String : JSON]) throws -> Void)? = nil,
                       other proc: ((inout JSON) throws -> Void)? = nil) throws
  ...
}

最通用的修改形式由以下两种方法提供

mutating func mutate(_ ref: JSONReference,
                     with proc: (inout JSON) throws -> Void) throws
mutating func mutate(_ ref: JSONReference,
                     array arrProc: ((inout [JSON]) throws -> Void)? = nil,
                     object objProc: ((inout [String : JSON]) throws -> Void)? = nil,
                     other proc: ((inout JSON) throws -> Void)? = nil) throws

这些方法通过函数 proc 修改引用 ref 指向的 JSON 值。proc 接收对该 JSON 值的引用,从而可以进行高效的、就地修改,而无需自动创建副本。

mutate 方法的第二种形式提供了特定的函数 arrProc 用于修改数组,objProc 用于修改对象,同样是以不创建副本的方式进行。对于所有其他值,将调用 proc

JSON Patch

JSON Patch 定义了一个 JSON 文档结构,用于表达应用于 JSON 文档的操作序列。每个操作都会修改 JSON 文档的某些部分。RFC 6902 规范支持的操作由枚举 JSONPatchOperation 实现

enum JSONPatchOperation: Codable, Hashable, CustomStringConvertible, CustomDebugStringConvertible {
  // add(path, value): Add `value` to the JSON value at `path`
  case add(JSONPointer, JSON)
  // remove(path): Remove the value at location `path` in a JSON value.
  case remove(JSONPointer)
  // replace(path, value): Replace the value at location `path` with `value`.
  case replace(JSONPointer, JSON)
  // move(path, from): Move the value at `from` to `path`. This is equivalent
  // to first removing the value at `from` and then adding it to `path`.
  case move(JSONPointer, JSONPointer)
  // copy(path, from): Copy the value at `from` to `path`. This is equivalent
  // to looking up the value at `from` and then adding it to `path`.
  case copy(JSONPointer, JSONPointer)
  // test(path, value): Compares value at `path` with `value` and fails if the
  // two are different.
  case test(JSONPointer, JSON)
  ...
}

结构体 JSONPatch 将操作捆绑到一个“patch 对象”中,提供将 patch 应用于 JSON 值的功能

struct JSONPatch: Codable, Hashable, CustomStringConvertible, CustomDebugStringConvertible {
  // Sequence of operations.
  let operations: [JSONPatchOperation]
  // Initializer based on a sequence of operations
  init(operations: [JSONPatchOperation]) { ... }
  // Decodes the provided data with the given decoding strategies.
  init(data: Data,
       dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
       floatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .throw,
       userInfo: [CodingUserInfoKey : Any]? = nil) throws { ... }
  // Decodes the provided string with the given decoding strategies.
  init(string: String,
       dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
       floatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .throw,
       userInfo: [CodingUserInfoKey : Any]? = nil) throws { ... }
  // Decodes the content at the provided URL with the given decoding strategies.
  init(url: URL,
       dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
       floatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .throw,
       userInfo: [CodingUserInfoKey : Any]? = nil) throws { ... }
  ...
  // Applies this patch object to `json` mutating `json` in place.
  func apply(to json: inout JSON) throws { ... }
  ...
}

以下代码显示了如何将 JSON patch 代码段加载到 patch 对象中并将其应用于 JSON 值

let jsonstr = """
  [
    { "op": "test", "path": "/a/b/c", "value": "foo" },
    { "op": "remove", "path": "/a/b/c" },
    { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
    { "op": "replace", "path": "/a/b/c", "value": 42 },
    { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
    { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
  ]
  """
let patch = try JSONPatch(string: jsonstr)
var json: JSON = ...
try json.apply(patch: patch)

合并 JSON 值

对称合并

枚举 JSON 的方法 isRefinement(of:) 定义了两个 JSON 值之间的关系。如果满足以下条件,则 a.isRefinement(of: b) 为 true

  1. ab 都是相同类型的 JSON 值,
  2. 如果 ab 是数组,则它们具有相同的长度 n,并且对于每个 i ∈ [0; n[,a[i].isRefinement(of: b[i]) 成立,
  3. 如果 ab 是对象,则对于 b 的每个成员 m 及其值 b[m]a 中都有一个成员 m 及其值 a[m],使得 a[m].isRefinement(of: b[m])
  4. 对于所有其他类型,ab 相同,即 a == b

这种关系直观地建模了:每当可以从 b 中的给定位置(或 JSON 指针)读取值时,也可以从 a 中的相同位置读取值,并且为 a 读取的值是为 b 读取的值的细化。

以下示例展示了这种关系

let a = try JSON(string: #"""
  {
    "a": [1, { "b": 2 }],
    "c": { "d": [{}] }
  }
"""#)
let b = try JSON(string: #"""
  {
    "a": [1, { "b": 2, "e": 4 }],
    "c": { "d": [{"f": 5}] }
  }
"""#)
b.isRefinement(of: a)  true

枚举 JSON 提供了一个方法 merging(value:) 用于合并两个 JSON 值 ab,使得合并结果 a.merging(value: b) 是“最小的” JSON 值,它是 ab 的细化。如果不存在这样的合并值,则 merging(value:) 将返回 nil。这是一个例子

let c = try JSON(string: #"""
  { "a": [1, {"e": 8}],
    "c": {"f": "hello"},
    "g": 9 }
"""#)
a.merging(value: c) 

{
  "a": [1, { "b": 2, "e": 8 }],
  "c": { "d": [{}], "f": "hello" },
  "g": 9
}

直观地,merging(value:) 通过将所有不存在的值添加到合并值,并在可能的情况下合并重叠值来组合两个 JSON 值。每当无法合并两个值时,merging(value:) 将通过返回 nil 而失败。

覆盖合并

另一种方法 overriding(with:) 以不同的方式合并两个 JSON 值,当合并在其他情况下会失败时,允许作为参数传递的 JSON 值覆盖接收者的值。与方法 merging(value:) 相比,组合数组不需要数组具有相同的长度。结果数组始终具有两个数组中最长数组的长度,并且只要有两个元素可用,就使用 overriding(with:) 组合各个元素。

这是一个如果改用 merging(value:) 会失败的示例

let d = try JSON(string: #"""
  {
    "a": [1, { "e": 2 }, 3],
    "c": { "d": "hello" },
    "f": 5
  }
"""#)
a.overriding(with: d)

{
  "a": [1, { "b": 2, "e": 2 }, 3],
  "c": { "d": "hello" },
  "f": 5
}

JSON Merge Patch

DynamicJSON 提供了对 JSON Merge Patch 的基本支持,由 RFC 7396 定义。

JSON 合并 patch 文档描述了要对目标 JSON 文档进行的更改,使用的语法与正在修改的文档非常相似。合并 patch 文档的接收者通过将提供的 patch 的内容与目标文档的当前内容进行比较来确定所请求的确切更改集。如果提供的合并 patch 包含目标中不存在的成员,则会添加这些成员。如果目标确实包含该成员,则该值将被替换。合并 patch 中的空值被赋予特殊含义,以指示删除目标中的现有值。

将合并 patch 文档应用于 JSON 值的算法由枚举 JSON 的方法 merging(patch:) 实现。

// Merges this JSON value with the given JSON value `patch` recursively. Objects are
// merged key by key with values from `patch` overriding values of the object represented
// by this JSON value. All other types of JSON values are not merged and `patch` overrides
// this JSON value.
func merging(patch: JSON) -> JSON { ... }

将合并 patch 文档应用于 JSON 值的实现不会修改现有的 JSON 值。它通过将旧值与合并 patch 文档合并,从头开始构造一个新的 JSON 值。

验证 JSON 数据

DynamicJSON 实现了 JSON Schema,由 2020-12 Internet Draft 规范 定义,用于验证 JSON 数据。该框架很灵活,允许为未来的修订进行扩展。

实现概述

JSON Schema 由枚举 JSONSchema 表示。可以从文件加载 JSON Schema 值,也可以从字符串或数据对象中解码它们。在 Schema 验证的上下文中,顶层 Schema 值通过类 JSONSchemaResource 进行管理,该类预处理和验证 Schema 值,并为其提供标识。通常,直接使用 JSONSchemaResource 对象更容易。JSON Schema 由 JSONSchemaIdentifier 值标识,这些值本质上是具有 JSON Schema 特定方法的 URI。JSONSchemaIdentifier 值可以是绝对的或相对的,它可以是基本 URI(即,它引用顶层 Schema),也可以是非基本 URI,因此通过 URI 片段引用嵌套在另一个 Schema 中的 Schema。

Schema 的语义由其方言定义。Schema 方言由 URI 标识。Schema 值通过属性 schema 提供对其方言标识符的访问。如果未提供标识符,则假定为默认值(目前对于顶层 Schema 为 JSONSchemaDialect.draft2020,对于嵌套 Schema 为封闭 Schema 的方言)。Schema 方言由 JSONSchemaDialect 协议的实现表示。JSONSchemaDialect 实现的一个关键职责是提供工厂方法 validator(for:, in:),用于为此方言创建验证器对象。验证器对象实现协议 JSONSchemaValidator,该协议提供一个 validate() 方法,该方法接受 JSON 实例并返回 JSONSchemaValidationResult 值。最终调用此方法来验证 JSON 值。

整个 Schema 验证过程由 JSONSchemaRegistry 对象启动和控制。JSON Schema 注册表定义

JSONSchemaRegistry 的大部分功能是关于通过注册受支持的方言、插入可用的 Schema 资源和设置 Schema 提供程序来自动发现 Schema 资源来配置注册表对象。一旦配置了注册表,就可以调用方法 validator(for:, dialect:) 来获取给定 Schema 资源和默认方言的 JSONSchemaValidator 对象。然后,可以使用此验证器对象来验证任意数量的 JSON 实例。

以下示例显示了验证的一般用法

// Create a new schema registry
let registry = JSONSchemaRegistry()
// Register a schema resource from a string literal
try registry.register(resource: JSONSchemaResource(string: #"""
  {
    "$id": "https://example.com/schema/test",
    "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "prop1": {
        ...
      }
    }
  }
"""#))
...
// Load a schema resource from a file
try registry.loadSchema(from: URL(filePath: "/Users/objecthub/foo.json"))
...
// Make JSON schema stored in json files under the given directory discoverable
registry.register(provider:
  StaticJSONSchemaFileProvider(
    directory: URL(filePath: "/Users/objecthub/myschema"),
    base: JSONSchemaIdentifier(string: "http://example.com/schemas")!))
...
// Obtain a validator for a schema
guard let validator = try? registry.validator(for: "https://example.com/schema/test") else {
  // Throw error stating that the schema could not be found
}
// Validate a JSON instance `json`
let result = validator.validate(json)
print("valid = \(result.isValid)")

Schema 验证器返回 JSONSchemaValidationResult 值。这些是容器,提供对验证过程中收集的信息的访问。JSONSchemaValidationResult 值封装了以下信息

为了弄清楚验证是否成功,只需使用 JSONSchemaValidationResult 的属性 isValid 即可。如果未找到验证错误,则返回 true。否则,属性 errors 提供对找到的验证错误的访问。在验证期间收集的其他注释在下面讨论。

验证 API

如果应用程序仅针对少量固定 Schema 验证 JSON 实例(例如,在应用程序启动时静态提供),则使用上面介绍的低级 API 将是多余的。对于如此简单的用例,枚举 JSON 提供了以下便捷方法

enum JSON: Hashable, ... {
  // Returns true if this JSON document is valid for the given JSON schema (using
  // `registry` for resolving references to schema referred to from `schema`).
  func valid(for schema: JSONSchema,
             dialect: JSONSchemaDialect? = nil,
             using registry: JSONSchemaRegistry? = nil) -> Bool
  
  // Returns a schema validation result for this JSON document validated against the
  // JSON schema `schema` (using`registry` for resolving references to schema
  // referred to from `schema`).
  func validate(with schema: JSONSchema,
                dialect: JSONSchemaDialect? = nil,
                using registry: JSONSchemaRegistry? = nil) throws -> JSONSchemaValidationResult
  
  // Returns true if this JSON document is valid for the given JSON schema (using
  // `registry` for resolving references to schema referred to from `schema`).
  func valid(for resource: JSONSchemaResource,
             dialect: JSONSchemaDialect? = nil,
             using registry: JSONSchemaRegistry? = nil) -> Bool
  
  // Returns a schema validation result for this JSON document validated against the
  // JSON schema `schema` (using`registry` for resolving references to schema
  // referred to from `schema`).
  func validate(with resource: JSONSchemaResource,
                dialect: JSONSchemaDialect? = nil,
                using registry: JSONSchemaRegistry? = nil) throws  -> JSONSchemaValidationResult
  ...
}

如果参数 registry 设置为 nil,这些验证方法会按需创建新的注册表,仅注册提供的 Schema 或 Schema 资源。为了使用非自包含的 Schema,因此有必要首先设置合适的注册表,并通过 registry 参数传入。或者,如果单个、共享的全局注册表就足够了,则可以使用默认注册表 JSONSchemaRegistry.default

元数据和默认值

格式注释

JSON Schema 的 format 注释,即 JSON 字符串值具有特定格式的声明,正在收集并通过 formatConstraints 属性提供,该属性是 JSONSchemaValidationResult 值。每个约束都是 Annotation<FormatConstraint> 的实例,提供对字段 valueLocatedJSON 值)、location(在 Schema 中)、message.formatmessage.valid 的访问,其中 message.validBool? 类型的值。true 指的是有效约束,false 指的是无效约束,nil 指的是无法验证的约束。以下代码打印出所有无效约束

val res: JSONSchemaValidationResult = ...
for constraint in res.formatConstraints where constraint.message.valid == false {
  print("value \(constraint.value) does not match format '\(constraint.message.format)'")
}

如果无效的格式约束应导致验证错误,则需要启用词汇表 https://json-schema.fullstack.org.cn/draft/2020-12/meta/format-annotation。这可以通过创建自定义 JSONSchemaDraft2020.Dialect 值来实现,该值具有 JSONSchemaDraft2020.Vocabulary 类型的词汇表,其 format 属性设置为 true。由于这种方言经常有用,因此可以通过 JSONSchemaDialect.draft2020Format 获得预配置的方言值。定义以此方言作为其默认值的注册表将始终验证格式注释。

默认注释

JSON Schema 的 default 注释,即如果未显式定义属性,则声明属性具有给定的默认值,正在收集并通过 defaults 属性提供,该属性是 JSONSchemaValidationResult 值。defaults 提供从 JSONLocation 值到元组 (exists: Bool, values: Set<JSON>) 的映射。元组的 exists 组件说明在此位置是否存在值(因此,不需要注入默认值)。values 组件提供一组 JSON 值,这些值都适合作为默认值(Schema 可以定义多个备选默认值)。以下代码打印出在验证过程中计算出的默认值

let schema = try JSONSchema(string: #"""
  {
    "$id": "https://objecthub.com/example/person",
    "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema",
    "title": "person",
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "minLength": 1
      },
      "birthday": {
        "type": "string",
        "format": "date"
      },
      "numChildren": {
        "type": "integer",
        "default": 0
      },
      "address": {
        "oneOf": [
          { "type": "string", "default": "12345 Mcity" },
          { "$ref": "#address",
            "default": { "city": "Mcity", "postalCode": "12345" } }
        ]
      },
      "email": {
        "type": "array",
        "maxItems": 3,
        "items": {
          "type": "string",
          "format": "email"
        }
      }
    },
    "required": ["name", "birthday"],
    "$defs": {
      "address": {
        "$anchor": "address",
        "type": "object",
        "properties": {
          "street": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "postalCode": {
            "type": "string",
            "pattern": "\\d{5}"
          }
        },
        "required": ["city", "postalCode"]
      }
    }
  }
"""#)
/// `instance0` is a valid instance of `schema`
let instance0: JSON = [
  "name": "John Doe",
  "birthday": "1983-03-19",
  "numChildren": 2,
  "email": ["john@doe.com", "john.doe@gmail.com"]
]
instance0.valid(for: schema) ⇒ true
/// `instance1` is not a valid instance of `schema`
let instance1: JSON = [
  "name": "John Doe",
  "email": ["john@doe.com", "john.doe@gmail.com"]
]
instance1.valid(for: schema) ⇒ false
/// `instance2` is a valid instance of `schema`
let instance2: JSON = [
  "name": "John Doe",
  "birthday": "1983-03-19",
  "address": "12 Main Street, 17445 Noname"
]
let res2 = try instance2.validate(with: schema)
res2.isValid ⇒ true
for (location, (exists, values)) in res2.defaults {
  if exists {
    print("\(location) exists; defaults: \(values)")
  } else {
    print("\(location) does not exist; defaults: \(values)")
  }
}

此代码末尾的循环打印出以下文本

$['numChildren'] does not exist; defaults: [0]
$['address'] exists; defaults: [
  "12345 Mcity",
  {
    "postalCode" : "12345",
    "city" : "Mcity"
  }
]

属性元数据

JSON Schema 的属性元数据注释,例如 deprecatedreadOnlywriteOnly,正在收集并通过 tags 属性提供,该属性是 JSONSchemaValidationResult 值。具有元数据注释的验证值中的每个位置都包含在此数组中,并具有 Annotation<MetaTags> 类型的条目,提供对字段 valueLocatedJSON 值)、location(在 Schema 中)、message.deprecatedmessage.readOnlymessage.writeOnly 的访问。deprecatedreadOnlywriteOnly 是布尔属性。

要求

构建 DynamicJSON 框架需要以下技术。库和命令行工具都可以使用 XcodeSwift Package Manager 构建。

版权

作者:Matthias Zenger (matthias@objecthub.com)
版权所有 © 2024 Matthias Zenger。保留所有权利。