2022 年 8 月 31 日,星期三 ·6分钟阅读

使用动态引用来支持泛型

返回博客
格雷格·丹尼斯

我们收到的最常见问题之一是如何在 JSON Schema 中表示强类型编程语言中的概念。类层次结构、多态、泛型等。这些想法定义了强类型语言,并影响了我们的数据模型。

本文中的主题可以应用于任何编程范式,其中您拥有一个定义的数据模型并支持泛型之类的概念。这可能是关于编程语言中的数据建模与 JSON Schema 之间关系的非连续系列中的第一个。

今天,我想介绍泛型或模板,或者您可能听到的其他一些标签。首先,让我们介绍一下我所说的“泛型”的意思。这不是对它们的教学,我只是想确保我们都在同一个页面上。

泛型

我所说的“泛型”是指许多编程语言中的一种功能,它可以创建一种类型,该类型需要一个或多个辅助类型的信息才能完整。在面向对象的编程中,泛型也可以应用于服务和数据模型,但由于我们使用 JSON Schema 来描述数据模型,因此我们可以相当确定,我们在此用例中关心的泛型是包装器和容器。

在 .Net (C#) 中,这些用类型名称末尾的尖括号表示,例如 List<T>Dictionary<TKey, TValue>,其中 TTKeyTValue 代表辅助类型。(这些示例都是容器类型,但您也可以对诸如 Cloud Events(例如 CloudEvent<T>)之类的信封类型执行此操作。)

在 C++ 中,这些类型被称为“模板”,并用关键字 template 表示,后面是附加的类型信息,也在尖括号内

1template <class T>
2class List { ... }
3
4template <class TKey, class TValue>
5class Dictionary { ... }

Typescript 也具有此概念,并且主要遵循 C# 语法。

然后通过定义所需的辅助类型来完成这些类型。这通常通过用辅助类型替换类型占位符(例如 T)来完成,因此对于 C# 示例,它是 List<T>Dictionary<string, int>。一个有趣的结果是,泛型类型不能单独实例化:它需要辅助类型,以便编译器或脚本引擎(或运行代码的任何东西)可以了解有关该类型的各种信息,例如内存占用。

因此,问题是,如何在 JSON Schema 中表示部分定义的类型。

$dynamicRef$dynamicAnchor 表示泛型

动态关键字(统称为 $dynamic*)允许引用,这些引用通常在评估时才能解析,不像 $ref,它可以仅使用模式静态解析。这在模式还定义条件(if/then/else)时最明显,因为条件会根据正在评估的 JSON 实例更改解析。

但是,为了支持泛型,我们要以稍微不同的方式使用这种动态行为。我们将使用一个两步过程的策略。

  1. 对于泛型本身,我们将编写一个具有引用的模式,该引用最初解析为始终失败验证的子模式。
  2. 对于每个派生,我们将编写一个子模式,该子模式
    • 从 #1 中引用泛型模式
    • 为同一个引用定义一个新的子模式,该子模式描述辅助类型

为了查看它的实际效果,让我们为上面的 List<T> 编写模式。然后我们将再编写两个模式,它们将使用它来帮助我们定义 List<string>List<int>

泛型模式:List<T>

我们从简单的事物列表开始。

schema
{ "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema", "$id": "https://json-schema.example/list-of-t", "type": "array"}

现在我们定义项。这是 $dynamic* 为我们做一些工作的地方。

schema
{ "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-t", "$defs": { "content": { "$dynamicAnchor": "T", "not": true } }, "type": "array", "items": { "$dynamicRef": "#T" }}

注意 我在这里使用了 T 来匹配 C# 中的 List<T>,以便更好地说明正在发生的事情。 你可以根据需要随意命名它。

如果我们仅针对此模式验证实例,"$dynamicRef": "#T" 将解析为包含 "$dynamicAnchor": "T" 的子模式,该子模式我们在 /$defs/content 中包含了。 在这种情况下,$dynamicRef$dynamicAnchor 的工作原理与 $ref$anchor 一样。

"not": true 表示所有实例都将验证失败。 通常,为了确保所有实例验证失败,我们会使用 false 模式,但在这种情况下,我们需要包含一个动态锚点,因此简单的 false 不起作用。 我认为 "not": true 可能是最干净的替代方案,但你也可以使用类似于 "allOf": [ false ] 的东西,如果你觉得这样更合理。

注意 空数组对于此模式仍然会通过验证,但任何包含项目的数组都将失败。

你还可以使用多个动态锚点来支持像 Dictionary<TKey, TValue> 这样的类型,它们需要多个辅助类型。

schema
{ "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-t", "$defs": { "key": { "$dynamicAnchor": "TKey", "not": true }, "value": { "$dynamicAnchor": "TValue", "not": true } }, "type": "array", "items": { "type": "object", "properties": { "key": { "$dynamicRef": "#TKey" }, "value": { "$dynamicRef": "#TValue" } } }}

对于泛型类型,我们就介绍到这里。 接下来,我们定义内容时,将会展现其真正的魔力。

定义内容

如前所述,我们需要一个引用 list-of-t 并且还提供 T 的定义的模式。 让我们为 List<string> 写一个模式。

schema
{ "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-string", "$defs": { "string-items": { "$dynamicAnchor": "T", "type": "string" } }, "$ref": "list-of-t"}

这里是如何工作的

  1. 当评估开始时,它会创建一个“动态作用域”,该作用域从根架构开始 (list-of-string) 并在整个评估过程中保持不变。
  2. 这个根架构定义了一个 "$dynamicAnchor": "T".
  3. 然后评估 $ref 到通用架构,list-of-t. 这是一个新的词法作用域,但动态作用域保持不变。
  4. 通用架构还声明 "$dynamicAnchor": "T",但该动态锚点已定义,因此忽略新声明。
  5. 当评估命中 "$dynamicRef": "#T" 时,它使用动态作用域开头的第一个。

如果我们想要 int 项而不是 string,我们只需要创建一个新的架构,其中 $dynamicAnchor 的子架构定义一个整数。

schema
{ "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-int", "$defs": { "int-items": { "$dynamicAnchor": "T", "type": "integer" } }, "$ref": "list-of-t"}

结论

通过使用 $dynamicRef$dynamicAnchor,您无需为结构相同但内容类型不同的类编写完整的模式。相反,您可以编写一个结构的局部可重用模式,这使得完全定义的模式显著更小,更容易维护。

旁注 在 Draft 2020-12 中,为了使 $dynamicRef 起作用,在通用模式中包含 $dynamicAnchor 是必需的。在未来的版本中,这一要求将被移除,因为它不是严格必要的:任何解析尝试都将简单地失败。(这一要求是其 Draft 2019-09 前身 $recursive* 的遗留问题。)但是,对于对泛型类型的特定建模应用,我仍然会保留它,因为它可以作为无法实例化泛型类型的类比,例如 List<T>。最终结果是一样的(验证失败),但我认为更明确地包含它描述了意图。

封面照片由 Nick FewingsUnsplash 上拍摄,我做了一些编辑。 😁