使用动态引用来支持泛型
我们收到的最常见问题之一是如何在 JSON Schema 中表示强类型编程语言中的概念。类层次结构、多态、泛型等。这些想法定义了强类型语言,并影响了我们的数据模型。
本文中的主题可以应用于任何编程范式,其中您拥有一个定义的数据模型并支持泛型之类的概念。这可能是关于编程语言中的数据建模与 JSON Schema 之间关系的非连续系列中的第一个。
今天,我想介绍泛型或模板,或者您可能听到的其他一些标签。首先,让我们介绍一下我所说的“泛型”的意思。这不是对它们的教学,我只是想确保我们都在同一个页面上。
泛型
我所说的“泛型”是指许多编程语言中的一种功能,它可以创建一种类型,该类型需要一个或多个辅助类型的信息才能完整。在面向对象的编程中,泛型也可以应用于服务和数据模型,但由于我们使用 JSON Schema 来描述数据模型,因此我们可以相当确定,我们在此用例中关心的泛型是包装器和容器。
在 .Net (C#) 中,这些用类型名称末尾的尖括号表示,例如 List<T>
或 Dictionary<TKey, TValue>
,其中 T
、TKey
和 TValue
代表辅助类型。(这些示例都是容器类型,但您也可以对诸如 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 中引用泛型模式
- 为同一个引用定义一个新的子模式,该子模式描述辅助类型
为了查看它的实际效果,让我们为上面的 List<T>
编写模式。然后我们将再编写两个模式,它们将使用它来帮助我们定义 List<string>
和 List<int>
。
泛型模式:List<T>
我们从简单的事物列表开始。
现在我们定义项。这是 $dynamic*
为我们做一些工作的地方。
注意 我在这里使用了 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>
这样的类型,它们需要多个辅助类型。
对于泛型类型,我们就介绍到这里。 接下来,我们定义内容时,将会展现其真正的魔力。
定义内容
如前所述,我们需要一个引用 list-of-t
并且还提供 T
的定义的模式。 让我们为 List<string>
写一个模式。
这里是如何工作的
- 当评估开始时,它会创建一个“动态作用域”,该作用域从根架构开始 (
list-of-string
) 并在整个评估过程中保持不变。 - 这个根架构定义了一个
"$dynamicAnchor": "T"
. - 然后评估
$ref
到通用架构,list-of-t
. 这是一个新的词法作用域,但动态作用域保持不变。 - 通用架构还声明
"$dynamicAnchor": "T"
,但该动态锚点已定义,因此忽略新声明。 - 当评估命中
"$dynamicRef": "#T"
时,它使用动态作用域开头的第一个。
如果我们想要 int
项而不是 string
,我们只需要创建一个新的架构,其中 $dynamicAnchor
的子架构定义一个整数。
结论
通过使用 $dynamicRef
和 $dynamicAnchor
,您无需为结构相同但内容类型不同的类编写完整的模式。相反,您可以编写一个结构的局部可重用模式,这使得完全定义的模式显著更小,更容易维护。
旁注 在 Draft 2020-12 中,为了使 $dynamicRef
起作用,在通用模式中包含 $dynamicAnchor
是必需的。在未来的版本中,这一要求将被移除,因为它不是严格必要的:任何解析尝试都将简单地失败。(这一要求是其 Draft 2019-09 前身 $recursive*
的遗留问题。)但是,对于对泛型类型的特定建模应用,我仍然会保留它,因为它可以作为无法实例化泛型类型的类比,例如 List<T>
。最终结果是一样的(验证失败),但我认为更明确地包含它描述了意图。
封面照片由 Nick Fewings 在 Unsplash 上拍摄,我做了一些编辑。 😁