65.9K
CodeProject 正在变化。 阅读更多。
Home

JSON Schema 入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (26投票s)

2018年3月9日

CPOL

23分钟阅读

viewsIcon

58411

downloadIcon

655

JSON Schema 可能是解决 JSON 文件大多数问题的答案。我们将从实践角度探索 JSON Schema。

目录

  1. 引言
  2. 背景
  3. JSON Schema
    1. 规范 v3
    2. 规范 v4
    3. 规范 v6
    4. 规范 v7
    5. Schema 约束
    6. Visual Studio Code
    7. JSON.NET Schema
  4. 编辑器
  5. 使用代码
  6. 关注点
  7. 参考文献
  8. 历史

引言

在本文中,我们将研究如何使用 WPF 创建一个出色的 JSON schema 编辑器。为了充分理解代码,我们需要对 JSON schema 有一些恰当的介绍。我们将看到 JSON schema 对于对 JSON 文件施加一些约束非常有用。没有 schema,JSON 文件可以(根据定义)包含任意数据。

除了对 JSON 文件施加一些限制和要求的好处之外,我们还可以从创建 JSON schema 文件中获得另一个直接好处:我们可以帮助用户理解有哪些可用选项以及结构需要如何才能正确。此外,所有内容都可以用一些可能显示在帮助文本中的附加信息进行描述和增强。

JSON schema 最好的部分之一是它完全用 JSON 编写。甚至还有一个 JSON schema 描述了 JSON schema 文件应该是什么样子。

在深入了解 JSON schema 的细节之前,我们将快速回顾一下我自己在 JSON schema 文件方面的背景。最重要的部分之一将讨论 JSON schema 规范各个版本之间的差异。

背景

实际上,我一年多以前就写了这篇文章,但我一直没时间发表。现在情况将有所改变。我还会尝试相应地更新内容,因为在撰写本文时,v4 是最新的规范。现在我们有了 v7。从 v4 到 v6 的迁移以及与 v7 的差异将在本文未来的修订版中进行解释。

最近我创建了一个应用程序,需要复杂的条件配置才能正常运行。配置以 JSON 文件的形式提供。我立即面临的问题如下:

  • 如何以灵活的方式检查或验证输入
  • 如何告知用户缺少或无效的部分
  • 如何在不陷入依赖地狱的情况下支持多个版本
  • 如何提供一个允许创建配置文件而无需重复代码的编辑器

正如感兴趣的读者可能猜到的那样,JSON schema 为我解决了这些问题。

在本文中,我们将仔细研究 JSON schema、它的属性、可用库和不足之处。我们的目标是根据给定的 schema 编写一个图形化的 JSON 编辑器。由于 JSON schema 是以 JSON schema 的形式定义的,我们也可以使用编辑器来编辑 schema 本身。

话不多说:全速前进!让我们探索 JSON schema。

JSON Schema

JSON schema 是一种可用于将约束和要求形式化到 JSON 文件的格式。我们可以指定:

  • 存在哪些属性
  • 可以使用哪些类型
  • 属性是否必需
  • JSON 是如何组成的
  • 为什么这些属性是相关的

官方网站提供了规范和一些示例。主要问题之一是没有单一的信息点。JSON schema 规范由不同的版本组成,最常用的是早期的 v3 和当前的 v4 规范。我们将在接下来的两节中讨论这两个规范之间的差异。

规范 v3

JSON schema 规范的第三次迭代取得了巨大成功。即使在今天,许多系统仍然依赖此版本,而不是更新到更近期的草案。我们将看到这是有原因的,因为并非所有更改都可能受到所有人的赞赏。

首先让我们从 JSON schema 的基础知识开始,即在 v3 和 v4 中都适用并且可以认为是任何 JSON schema 的基本要素。

JSON schema 定义了 JSON 文件的内容。以递归方式,此定义也在 JSON 文件中执行。最值得注意的是,此定义意味着:

  • 使用的数据类型
  • 允许的属性(对于对象)
  • 允许的(对于数组)
  • 允许的值(对于枚举,enum
  • 项目是必需的还是可选的
  • 最小值最大值(对于数字)
  • 要遵循的模式(对于字符串)
  • ...

除了这些显而易见(和一些不那么显而易见)的约束之外,我们还可以包含一些元数据,例如字段的描述。当验证失败或 schema 构成编辑器中自动完成的基础时,描述可能会有所帮助。

一个 schema 可以像以下代码一样简单,它只指定一个应该是一个字符串的 JSON。

 

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-03/schema#",
  "type": "string"
}

$schema 键不是必需的,但是有助于识别正确的 schema 版本。在这种情况下,我们使用规范 v3 - 稍后我们将通过将 URL 更改为 https://json-schema.fullstack.org.cn/draft-04/schema# 来引用 v4。

为字符串添加描述很简单:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-03/schema#",
  "description": "This is a really simple JSON file - just enter a string!",
  "type": "string"
}

当然,大多数 JSON 文件将不仅仅是一个原始值(即复杂值)。所以让我们看看带有某些属性的对象的定义可能是什么样子。

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-03/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "type": "string"
    },
    "bar": {
      "type": "number"
    }
  }
}

之前的 JSON schema 验证了以下 JSON 片段:

// valid:
{ }

// valid:
{ "foo": "bar" }

// valid:
{ "bar": 3 }

// valid:
{ "baz": false }

// valid:
{ "foo": "bar", "bar": 4.1, "baz": [] }

然而,即将到来的 JSON 片段都是无效的:

// invalid:
[]

// invalid:
{ "foo": false }

// invalid:
{ "bar": "foo" }

// invalid:
{ "foo": "bar", "bar": [] }

重要提示:我们仍然没有看到如何在给定属性上施加除类型之外的约束或指定替代方案。让我们从后者开始。常见的情况是数组中的项可以是多种类型之一。在这里,type 字段可以与数组一起使用。

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-03/schema#",
  "type": "array",
  "items": {
    "type": [
      { "type": "string" },
      { "type": "number" }
    ]
  }
}

现在假设具有类似约束的数组在我们的 schema 中更频繁地出现。我们从以下示例开始:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-03/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "type": "array",
      "items": {
        "type": [
          { "type": "string" },
          { "type": "number" }
        ]
      }
    },
    "bar": {
      "type": "array",
      "items": {
        "type": [
          { "type": "string" },
          { "type": "number" }
        ]
      }
    }
  }
}

我们显然违反了“不要重复自己”(DRY)原则。这将对 schema 文件的可维护性产生负面影响。幸运的是,JSON schema 规范定义了一种声明定义以供引用重用的方法。

我们现在重构之前的示例以使用此类定义。

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-03/schema#",
  "definitions": {
    "MyArray": {
      "type": "array",
      "items": {
        "type": [
          { "type": "string" },
          { "type": "number" }
        ]
      }
    }
  },
  "type": "object",
  "properties": {
    "foo": {
      "$ref": "#/definitions/MyArray"
    },
    "bar": {
      "$ref": "#/definitions/MyArray"
    }
  }
}

$ref 字段通过引入引用的定义的所有已定义属性来放置给定的引用。这是我们仍然遵守 DRY 的选择方式。这种引用的美妙之处在于,其值是一个 URI,它不仅可以引用本地定义(在同一文件中声明),还可以引用其他文件中的定义。这些文件可能在互联网上可用,或者取自本地文件系统(通常我们会使用相对路径)。

回到之前的一个问题:我们如何使对象的某些属性成为强制性的?答案很简单:我们声明这些属性是必需的!在规范 v3 中,它看起来如下:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-03/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "type": "string",
      "required": "true"
    },
    "bar": {
      "type": "number"
    }
  }
}

然而,考虑到前面给出的创建定义的方式,我们发现这种声明所需属性的方式既不可伸缩也不易于分解。因此,它在规范 v4 中进行了更改。是时候继续使用最新规范了!

规范 v4

有什么可以改进的呢?首先,之前规范的一个问题是模块化被打破了。这个问题可能不容易直接发现,因此它被发布到规范的早期版本中。

JSON schema 中的模块化是如何给出的?模块由单独的 JSON schema 文件构建,其中一个文件引用另一个文件。在 JSON schema v3 中,我们无法编写例如一个可以在多个其他文件中重用的独立模块。原因在于,例如,如果模块被引用文件要求,则必须在目标模块中指定诸如这些属性。理想情况下,我们希望在引用者的模块中指定只有引用者才知道的属性。

在规范 v4 中,我们有了一个新的 required 字段,可用于对象。该字段需要一个字符串列表,这些字符串表示对象的不同(必需)属性。属性不需要显式定义(仅当我们施加进一步的约束,例如特定类型,或元数据,例如描述时)。

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "type": "string"
    },
    "bar": {
      "type": "number"
    }
  },
  "required": [
    "foo"
  ]
}

如果我们回想 JSON Schema v3,我们知道我们“滥用”了带有数组的 type 字段来包含多个潜在类型。然而,这种方法不够优雅,也没有提供 JSON Schema 所期望的灵活性(和表达能力)。因此,规范 v4 不允许这样做。相反,以前的示例是:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "array",
  "items": {
    "oneOf": [
      { "type": "string" },
      { "type": "number" }
    ]
  }
}

oneOf 字段是许多用于细化混合类型用法的新关键字之一。其他关键字包括 anyOfallOf。这些主要是为了允许从一些固定定义中组合类型。模块化的最佳体现!

总的来说,规范 v4 都是关于模块化的;因此它还将规范拆分为核心规范、验证 schema、超 schema 等不同部分。核心规范只处理:

  • 强制支持 JSON 引用。
  • 定义规范(和内联)寻址。
  • 根据相等性、根 schema、子 schema、实例、关键字定义 JSON。
  • 定义 schema 到实例的关联。

验证 schema 旨在确保互操作性并将实例验证与子验证分离。

现在让我们看看如何为应该只具有遵循特定命名约定的一组预定义属性的对象设置 JSON schema。

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "enum": [
      	"bar",
      	"baz"
      ]
    }
  },
  "patternProperties": {
    "^[A-Za-z]+[A-Za-z0-9]*$": {}
  },
  "additionalProperties": false,
  "required": [ "foo" ]
}

前面的例子做了几件事。首先,我们再次包含(定义)了一个名为 foo 的显式属性(此属性是必需的),它必须是给定值之一(字符串 bar 或字符串 baz)。然后,我们禁止了除指定属性之外的其他属性,但是,这仍然包括无限数量(至少理论上)的属性。任何由对象声明的、符合上述 schema 的有效属性都必须具有构成有效标识符的属性,即每个属性名称必须以字母开头,并以字母或数字结尾。不允许有空格或特殊符号。

规范 v6

作者是不是不会数数?嗯,要么是这样,要么他们跳过了 v5!这类似于其他著名的版本跳过,例如 Windows 9、iPhone 2、Angular 3、ECMAScript 4……不管具体原因是什么:v5 从未发布,我们直接获得了 v6。这个版本的规范确实包含一些对 v4 的重大更改。这可能相当不幸。所以让我们看看细节:

总共有四个重大更改:

  • id$id 替换。原因:它现在不容易与名为 id 的实例属性混淆。
  • 同样,$id 替换 id,行为相同。原因:$ 前缀与其他两个核心关键字匹配。
  • 只允许在期望 schema 的地方指定 $ref。原因:现在可以描述名为 $ref 的实例属性,这在 v4 中是不可能的。
  • exclusiveMinimumexclusiveMaximum 从布尔值更改为数字,以符合关键字独立性原则。

上述最后两个属性的迁移应按以下方式进行:如果其中任何一个以前为 true,则将值更改为相应的 minimummaximum 值,并删除“minimum”/“maximum”关键字。

除了这些重大变化之外,还有一些有用的补充。我们也将简要回顾一下:

  • 现在布尔值可以在任何地方作为 schema 出现,而不仅仅是 additionalPropertiesadditionalItems。这里,true 等效于 {},而 false 等效于 {"not": {}}
  • 添加了 propertyNames
  • 添加了 contains 数组关键字,如果其 schema 至少验证一个数组项,则通过验证。
  • 添加了 const,它是一种更易读的单元素 enum 形式。
  • required 现在允许一个空数组,表示不需要任何属性。
  • dependencies 也如此。
  • 添加了 "format": "uri-reference"(符合 RFC 3986)。
  • 添加了 "format": "uri-template"(符合 RFC 6570)。
  • 添加了 "format": "json-pointer"(JSON Pointer 值,如 /foo/bar;不要将其用于 JSON Pointer URI 片段,如 #/foo/bar)。
  • 添加了 examples,不用于验证目的。

其中一个亮点无疑是可用数据类型数量的增加。现在我们实际上可以指定和区分几种类型的 URI。很好,但这肯定也会增加一些混乱。

规范 v7

v7 不像 v6 那么大。它主要可以看作是更新,而不是对早期版本的重大重写。因此,它与 draft-06 完全向后兼容。

总的来说,v7 添加了一些新关键字。为了完整性,我们列举一下:

  • 添加了 $comment,用于给 schema 维护者提供一些注释,与 description(面向最终用户)不同。
  • 添加了 ifthenelse 到验证中。
  • 现在 readOnly 从 Hyper-Schema 移到了验证中。
  • 还有一个 writeOnly 可用于验证。
  • contentMediaType 从 Hyper-Schema 移到了验证中。
  • 最后,contentEncoding 也一样。

除了所有这些新关键字之外,还引入了一些新格式。我们现在有诸如 iri (URI 的国际化等效项) 或 iri-reference 等格式。除了其他一些(更奇特的)格式,例如 uri-templateidn-emailidn-hostname 或前面提到的 json-pointer,我们最终还获得了 regex (ECMA 262 正则表达式)、date (RFC 3339 完整日期) 以及 time (RFC 3339 完整时间)。这些在 v3 之后已被删除。

Schema 约束

让我们更详细地查看我们可以设置的约束。首先,我们可以设置特定类型,例如 number,或者在 enum 中提供一组固定值。这两种可能性如下所示。

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "oneOf": [
    { "type": "number" },
    { "enum": [1, 2, 3, "hello"] }
  ]
}

对于不同的类型,我们有哪些选项?

布尔值

由于布尔值非常简单(只有 truefalse),因此没有其他约束。

数字

数字很棘手。我们可能想对它们施加一些约束。最突出的一个将是“是整数”。因此,我们实际上有另一种类型:integer,而不是某种神奇的字段。一个选择是通过只允许给定常数的倍数的值来对数字施加约束,例如,以下代码是数字 CSS font-weight 属性的完美匹配:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "description": "Validates the numeric value of the CSS font-weight property.",
  "type": "integer",
  "minimum": 100,
  "maximum": 900,
  "multipleOf": 100
}

默认情况下,范围边界(最小值和最大值)是包含性的,即我们有 minimum <= value <= maximum。我们可以通过相应地设置 exclusiveMinimum 和/或 exclusiveMaximum 属性来使这种关系成为排他性的。

让我们更改示例以使用排他性关系:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "description": "Validates the numeric value of the CSS font-weight property.",
  "type": "integer",
  "minimum": 0,
  "exclusiveMinimum": true,
  "maximum": 1000,
  "exclusiveMaximum": true,
  "multipleOf": 100
}

字符串

JSON 的本质(源自 JavaScript)是字符串密集型的。因此,对这种类型的值施加大量约束是有意义的。我们从范围开始(类似于前面讨论的数字范围)。这里,范围施加在长度上:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "string",
  "minLength": 1,
  "maxLength": 3
}

这只允许至少一个字符且最多三个字符的字符串。

然而,虽然长度限制可能不时方便,但更精确(且强大)的限制是正则表达式。在上面的示例中,没有什么能阻止用户只输入一个空格来满足我们的约束。这可能是想要的或不想要的。让我们假设约束应该是只包含 1 到 3 个字母的字符串。我们如何在 JSON schema 中设置这个约束?

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "string",
  "pattern": "^[A-Za-z]{1,3}$"
}

字符串的正则表达式不会自动被字符串边界限制。因此,如果我们要将表达式用于整个字符串(而不仅仅是其子集),我们需要在正则表达式语法中使用常见的起始 (^) 和结束 ($) 标记。

数组

我们可以在 JSON schema 中对数组所做的一件事是通过将 uniqueItems 属性设置为 true 来限制它只包含唯一项。此外,我们可以限制数组中项的数量,类似于字符串的长度:这里我们需要使用 minItemsmaxItems

此时我们不看示例,而是讨论 JSON schema 中关于数组更重要的领域。在 JSON schema 中,数组可以使用两种不同的机制进行验证:列表验证或元组验证。前者假定数组可以由一种或多种类型的元素组成(以任何顺序或组合),而后者定义了类型可能出现的固定顺序。

让我们先看一些列表验证:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "array",
  "items": {
    "type": "number"
  }
}

就这么简单,我们有一个数组,其中任何项都必须是一个数字。类似地,我们可以指定一个 oneOf 属性来包含多种类型。

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "array",
  "items": {
    "oneOf": [
      { "type": "number" },
      { "type": "string" }
    ]
  }
}

重要提示:这些类型可以以任何顺序和组合出现。因此,不仅 [1,2,3] 有效,["1", "2", "3"]["one", "two", 3][1, "two", 3] 也有效。

等等。

现在让我们看看元组验证的区别:

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "array",
  "items": [
    { "type": "number" },
    { "type": "string" }
  ]
}

我们直接在这里使用一个数组,而不是有一个带有 oneOf 操作的对象。与列表验证的区别在于,在上述示例中,只有 [1, "two", 3] 在此方案中是有效的。默认情况下,元组验证允许有额外的项(具有任意内容)。我们可以通过将 additionalItems 属性设置为 false 来禁用此行为。

对象

JSON 中的 O 代表 Object,这表明对象非常重要。因此,JSON schema 中的对象具有最多的约束可能性也就不足为奇了。我们已经研究了 propertiespatternProperties。我们还引入了 additionalProperties 标志来禁用允许未指定的属性。

类似于字符串和数组,我们可以对对象施加长度限制。在这里,启用此约束的属性称为 minPropertiesmaxProperties

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "type": "object",
  "minProperties": 1,
  "maxProperties": 2
}

前面给出的 JSON schema 将只允许包含一个或两个(任意)属性的对象。

我们能做的事情还有很多,但是与其重复官方规范,我们现在将把我们的知识应用到实践中。我们从一个流行的跨平台文本编辑器开始。

Visual Studio Code

文本编辑器领域是 JSON schema 知识非常有用的一个应用场景。例如,在 Visual Studio Code (VSCode) 中,JSON schema 是开箱即用的。为什么 JSON schema 会包含在 VSCode 的核心中呢?自然地,VSCode 的所有配置都通过 JSON 文件进行(这个选择相当自然,因为 VSCode 基于 Web 技术,而 JSON 无论如何都是事实上的标准)。此外,它还处理一系列框架、库和工具,这些也依赖 JSON 进行主要配置。提供一些验证、带有描述的帮助,甚至带有智能感知的自动补全对用户来说都是有益的,并有助于将 VSCode 定义为现代 Web 的编辑器。

VSCode 具有一种约定,可以在每个项目级别(也可以是每个用户和全局)自定义设置。这对于在团队中处理项目来说尤其有用。只需签入项目设置,同一团队的所有其他成员(作为前提条件也使用 VSCode)都会从中受益。

设置的两种不同范围处理如下:

  • 用户设置全局应用于任何打开的 VS Code 实例(存储在账户目录中;覆盖全局设置)
  • 项目(称为工作区)设置仅在项目打开时应用(存储在项目根目录的 .vscode 文件夹中;覆盖用户设置)

以下显示了一个位于某个项目 .vscode 文件夹中的 settings.json 文件。在此文件中,我们定义要应用的以下 schema。

{
  "json.schemas": [
    {
      "fileMatch": [
          "/.babelrc"
      ],
      "url": "http://json.schemastore.org/babelrc"
    },
    {
      "fileMatch": [
          "/*.foo.json"
      ],
      "url": "./myschema.json"
    }
  ]
}

第一个专门匹配 .babelrc 文件(使用从给定 URL 检索的 schema),而第二个应用于所有具有扩展名/后缀 .foo.json 的文件。由于这个似乎是相当自定义的,我们也没有在线上找到合适的 schema 源,而是从上面指定的相对路径获取一个本地的。

由于 VSCode 能够定义自定义 JSON 验证助手,我们可以享受智能感知(将显示已知属性)和内联验证(错误将在代码编辑器中显示)。因此,我们可以在没有 JSON schema 的情况下更高效地编写有效的 JSON。

现在假设我们确信 JSON schema 确实是我们自己 JSON 文件的正确选择(例如,某种需要使用 JSON 文件的配置系统)。

JSON.NET Schema

在 .NET 世界中,支持 JSON schema 的最简单选项是引用 JSON.NET Schema 包。标准 JSON.NET 包也带有一些对 JSON schema 的支持,但是,这种支持是有限的,并且仅适用于规范的 v3 版本。该库 100% 支持 JSON Schema 规范 v3 和 v4,因此是涵盖大多数用例的理想伴侣。它包含一些简单的验证帮助方法,并带来一些辅助类型,可以从现有的 .NET 类型生成 JSON Schema。

以下代码是开始使用该库的最低要求。

var schema = JSchema.Parse(someJsonSchemaString);

我们看到 API 实际上与标准 JSON.NET 库非常相似。让我们使用该包来实际完成一些工作!如何使用 schema 验证现有的 JSON 文件?

var obj = JObject.Parse(someJsonString);
var result = obj.IsValid(schema);

IsValid 扩展方法是一个方便的助手,可以将普通的 JSON.NET 库与 JSON.NET Schema 库一起使用。这个方便的助手等同于以下代码:

var obj = JObject.Parse(someJsonString);
var result = true;

using (var reader = new JSchemaValidatingReader(obj.CreateReader()))
{
    reader.ValidationEventHandler += (s, e) => result = false;
    reader.Schema = schema;

    while (reader.Read()) ;
}

验证事件处理程序功能强大。它允许我们输出确切的验证错误(如果需要)。在前面的代码中,我们决定跳过任何细节,一旦遇到任何错误就将结果设置为 false

JSchema 对象的 API 也允许我们检查 schema 文件本身的内容。例如,我们看到 schema 具有可选字段,例如 minimumPropertiesminimumItemsminimumLength。这些分别指附加到对象的最小属性数量、数组中的最小项数量和字符串中的最小字符数量。在 JSON.NET Schema 中,所有这些项都建模为 Int64?,因为它们必须是可选的(可能缺失/未指定)整数(自然地,在 JavaScript 中是 64 位)。

还有很多用例需要探索,但我们已经有足够的材料来编写本文前面宣布的专用 JSON 编辑器。

编辑器

我们现在将编写一个专用 JSON 编辑器,它使用 JSON schema 提供专业的用户体验。这不是一个带有高级语法高亮的简单文本编辑器,而是一种仅以 JSON (schema) 为基础创建复杂表单体验的方式。此表单的输出是一个 JSON 文件。编辑器使用 C# 和 WPF 框架编写。

我们首先定义主视图模型和相关的视图。视图模型到其视图的绑定是在应用程序全局级别完成的。这允许我们只使用视图模型,而无需直接创建或引用视图。

<Application x:Class="JsonEditor.App.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="clr-namespace:JsonEditor.App.ViewModels"
             xmlns:v="clr-namespace:JsonEditor.App.Views"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <DataTemplate DataType="{x:Type vm:MainViewModel}">
                <v:MainView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:FileViewModel}">
                <v:FileView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:ValidationViewModel}">
                <v:ValidationView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:TypeViewModel}">
                <v:TypeView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:ArrayViewModel}">
                <v:ArrayView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:BooleanViewModel}">
                <v:BooleanView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:EnumViewModel}">
                <v:EnumView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:NumberViewModel}">
                <v:NumberView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:ObjectViewModel}">
                <v:ObjectView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:StringViewModel}">
                <v:StringView />
            </DataTemplate>
        </ResourceDictionary>
    </Application.Resources>
</Application>

现在让我们从主窗口开始。流程如下:

  1. 用户加载一个 JSON 文件。
  2. 检索 $schema 属性。
  3. 如果 2. 成功,我们将该 JSON 文件用作 schema。
  4. 否则,我们采用默认 schema。
  5. 选定的 schema 显示在状态栏中,并可以更改它。

加载 JSON 文件可以通过拖放或点击工具栏中的按钮完成。拖放是通过行为实现的。

sealed class FileDropBehavior : Behavior<FrameworkElement>
{
    public IFileDropTarget Target
    {
        get { return AssociatedObject?.DataContext as IFileDropTarget; }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.AllowDrop = true;
        AssociatedObject.Drop += OnDrop;
        AssociatedObject.DragOver += OnDragOver;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.Drop -= OnDrop;
        AssociatedObject.DragOver -= OnDragOver;
    }

    private void OnDrop(Object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.FileDrop))
        {
            var files = e.Data.GetData(DataFormats.FileDrop) as String[];
            Target?.Dropped(files);
        }
    }

    private void OnDragOver(Object sender, DragEventArgs e)
    {
        e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) ? 
            DragDropEffects.Link : 
            DragDropEffects.None;
    }
}

加载过程如下所示。我们从常见的 JSON.NET 库中的普通 JSON 反序列化开始:

private static JToken ReadJson(String path)
{
    using (var sr = new StreamReader(path))
    {
        using (var reader = new JsonTextReader(sr))
        {
            return JToken.Load(reader);
        }
    }
}

根据定义,路径必须是有效的,因为该方法仅在路径验证后使用(通常在打开文件对话框之后;也可能来自前面显示的拖放文件助手)。

其理念是利用 JSON schema 来确定要添加的内容并提供实时验证。如果未提供 schema,则无法提供验证和建议。因此,我们仅使用 schema 来获取有用信息,而不是过多地限制用户。

最后,我们必须实现一个通用控件,允许用户选择数据类型并显示给定数据的验证报告。该通用控件还托管一个自定义控件,用于处理所选类型的数据。

支持以下类型:

  • 对象
  • 数组
  • 数字
  • 布尔值
  • 字符串
  • 枚举

每个都在编辑模式下力求用户友好。

布尔输入对应的 Xaml 代码如下所示,非常简单。

<UserControl x:Class="JsonEditor.App.Views.BooleanView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <ToggleButton IsChecked="{Binding Value}" />
</UserControl>

对于对象,Xaml 代码略大。本质上,我们可以将其归结为以下部分:

 

<DockPanel LastChildFill="True">
    <Button DockPanel.Dock="Bottom"
            Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}"
            Margin="5"
            Content="Add Property" />

    <Expander IsExpanded="{Binding IsExpanded}"
              Header="Object Properties">
        <ItemsControl ItemsSource="{Binding Children}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <materialDesign:Card Margin="5">
                        <DockPanel LastChildFill="True">
                            <TextBlock DockPanel.Dock="Top"
                                       Text="{Binding Name}"
                                       FontSize="20"
                                       ToolTip="{Binding Description}"
                                       Margin="5" />

                            <StackPanel HorizontalAlignment="Right" 
                                        DockPanel.Dock="Bottom"
                                        Orientation="Horizontal" 
                                        Margin="5">
                                <Button Style="{StaticResource MaterialDesignToolButton}"
                                        Command="{Binding DataContext.RemoveProperty, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}"
                                        CommandParameter="{Binding}"
                                        Padding="2 0 2 0"
                                        materialDesign:RippleAssist.IsCentered="True"
                                        Width="30"
                                        ToolTip="Remove Property">
                                    <materialDesign:PackIcon Kind="Delete" />
                                </Button>
                            </StackPanel>

                            <ContentControl Content="{Binding}" />
                        </DockPanel>
                    </materialDesign:Card>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Expander>
</DockPanel>

添加新属性需要先打开一个特殊对话框,这是通过 DialogHost 控件实现的。新属性的名称然后从组合框中选择,该组合框显示所有已知选项并允许输入任意文本。文本通过 JSON schema 文件(如果定义了任何 patternProperties)的帮助进行验证。

容器(通用控件)的代码也并非真正的黑魔法。

<DockPanel LastChildFill="True">
    <ContentControl Content="{Binding Validation}"
                    DockPanel.Dock="Top" />

    <ContentControl Content="{Binding Type}"
                    DockPanel.Dock="Top" />
    
    <ContentControl Content="{Binding Value}" />
</DockPanel>

总而言之,每个项目都由三个部分组成:其验证摘要、项目的类型(选择器)及其当前值。每个控件都从绑定引擎中选择。

使用代码

所附代码可以轻松定制,以仅支持特定 schema,或者更好地使用 UI 中的逻辑约束或选择来增强某些特殊 schema,这在纯 schema 文件中是不可能的。

该应用程序是一个小型 WPF 应用程序,它使您能够以图形方式创建或编辑 JSON 文件。默认情况下,JSON 文件是无 schema 的,接受任何输入,没有任何帮助。如果找到 "$schema" 注释,则选择给定的 schema。否则,始终可以手动设置新的 schema。

schema 决定了要显示哪些选项以及如何进行验证。

(待补充截图和用法)

兴趣点

在本文中,我们介绍了 JSON schema 规范,它是什么以及我们如何利用它来提供更好的用户体验。

我们已经看到编辑器从定义好的 schema 中获益良多。我们还能够用几行代码在 WPF 中构建一个简单的 JSON schema 编辑器。

就我个人而言,我亲眼目睹了无数次 JSON schema 改进解决方案或使其完全可行。例如,在一个应用程序中,我有一个庞大的配置系统。然而,这种系统的问题从来都不是配置变得太大,而是验证复杂性呈指数级增长。在代码中指定如何进行验证既繁琐又容易出错。JSON schema 要好得多,它会自动验证并附带可以显示给最终用户的正确错误消息。

另一个例子是某些 JSON 输入文件的 API 定义。在我遇到这个项目的项目中,整个规范都是用 Microsoft Word 文档编写的。不太愉快,对吧?开发人员必须查看 Word 文档,编写 JSON,最后针对生产系统测试 JSON。所有这些努力只是为了发现文档在某个特定区域是错误的(或过时的)……同样不愉快。JSON schema 可以有效地取代 Word 文档。此外,它不仅可以从源代码自动生成,还可以被文本编辑器使用或转译为 Markdown 或 PDF 以供打印参考。最棒的是:验证可以自动进行!不再需要手动上传和针对生产代码进行测试。

您认为 JSON schema 在哪里大放异彩?

参考文献

这是一份我发现有用或与本文中显示的信息相关的参考资料列表。

历史

  • v1.0.0 | 初次撰写 | 2017年1月22日
  • v1.0.1 | 初次发布 | 2018年3月9日
  • v1.0.2 | 添加来源 | 2018年3月13日
  • v1.1.0 | 关于 v6 和 v7 的信息 | 2018年4月30日
© . All rights reserved.