首頁 > web前端 > js教程 > 主體

繼續驗證資料:使用JSON-Schema進行驗證,第二部分

WBOY
發布: 2023-08-31 21:41:18
原創
625 人瀏覽過

繼續驗證資料:使用JSON-Schema進行驗證,第二部分

在本教學的第一部分中,您學習如何使用所有可用的驗證關鍵字來建立相當高階的架構。許多現實世界中的 JSON 資料範例比我們的使用者範例更複雜。嘗試將對此類資料的所有要求放入一個文件中可能會導致非常大的架構,並且還可能有大量重複。

建立您的架構

JSON 架構標準可讓您將架構分成多個部分。讓我們來看看新聞網站導航的資料範例:

{
  "level": 1,
  "parent_id": null,
  "visitors": "all",
  "color": "white",
  "pages": [
    {
      "page_id": 1,
      "short_name": "home",
      "display_name": "Home",
      "url": "/home",
      "navigation": {
        "level": 2,
        "parent_id": 1,
        "color": "blue",
        "pages": [
          {
            "page_id": 11,
            "short_name": "headlines",
            "display_name": "Latest headlines",
            "url": "/home/latest",
            "navigation": {
              "level": 3,
              "parent_id": 11,
              "color": "white",
              "pages": [
                {
                  "page_id": 111,
                  "short_name": "latest_all",
                  "display_name": "All",
                  "url": "/home/latest"
                },
                ...
              ]
            }
          },
          {
            "page_id": 12,
            "short_name": "events",
            "display_name": "Events",
            "url": "/home/events"
          }
        ]
      }
    },
    ...
  ]
}
登入後複製

上面的導航結構有點類似於您在網站 http://dailymail.co.uk 上看到的導航結構。您可以在 GitHub 儲存庫中查看更完整的範例。

資料結構複雜且遞歸,但描述該資料的模式非常簡單:

導航.json:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/navigation.json#",
  "title": "Navigation",
  "definitions": {
    "positiveIntOrNull": { "type": ["null", "integer"], "minimum": 1 }
  },
  "type": "object",
  "additionalProperties": false,
  "required": [ "level", "parent_id", "color", "pages" ],
  "properties": {
    "level":     { "$ref": "defs.json#/definitions/positiveInteger" },
    "parent_id": { "$ref": "#/definitions/positiveIntOrNull" },
    "visitors":  { "enum": [ "all", "subscribers", "age18" ] },
    "color":     { "$ref": "defs.json#/definitions/color" },
    "pages":     {
      "type": "array",
      "items": { "$ref": "page.json#" }
    }
  }
}
登入後複製

頁面.json:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/page.json#",
  "title": "Page",
  "type": "object",
  "additionalProperties": false,
  "required": [ "page_id", "short_name", "display_name", "path" ],
  "properties": {
    "page_id":      { "$ref": "defs.json#/definitions/positiveInteger" },
    "short_name":   { "type": "string", "pattern": "^[a-z_]+$" },
    "display_name": { "type": "string", "minLength": 1 },
    "path":         { "type": "string", "pattern": "^(?:/[a-z_\-]+)+$" },
    "color":        { "$ref": "defs.json#/definitions/color" },
    "navigation":   { "$ref": "navigation.json#" }
  }
}
登入後複製

defs.json:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/defs.json#",
  "title": "Definitions",  
  "definitions": {
    "positiveInteger": { "type": "integer", "minimum": 1 },
    "color": {
      "anyOf": [
        { "enum": [ "red", "green", "blue", "white" ] },
        { "type": "string", "pattern": "^#(?:(?:[0-9a-fA-F]{1,2})){3}$" }
      ]
    }
  }
}
登入後複製

查看上面的模式以及它們描述的導航資料(根據模式 navigation.json 有效)。主要要注意的是,模式 navigation.json 引用了模式 page.json,而後者又引用了第一個模式。

根據架構驗證使用者記錄的 JavaScript 程式碼可能是:

var Ajv = require('ajv');
var ajv = Ajv({
  allErrors: true,
  schemas: [
    require('./navigation.json'),
    require('./page.json'),
    require('./defs.json')
  ]
});

var validate = ajv.getSchema("http://mynet.com/schemas/navigation.json#");
var valid = validate(navigationData);
if (!valid) console.log(validate.errors);
登入後複製

所有程式碼範例均可在 GitHub 儲存庫中找到。

範例中使用的驗證器 Ajv 是 JavaScript 最快的 JSON 架構驗證器。我創建了它,因此我將在本教程中使用它。最後我們將比較它與其他驗證器的比較,以便您可以選擇適合您的驗證器。

任務

請參閱教學的第 1 部分,以了解如何安裝包含任務的儲存庫並測試您的答案。

使用「$ref」關鍵字的模式之間的參考

JSON-Schema 標準可讓您使用帶有 「$ref」 關鍵字的參考來重複使用架構的重複部分。正如您從導航範例中看到的,您可以引用位於以下位置的架構:

  • 在另一個檔案:使用在其「id」屬性中定義的架構 URI
  • 在另一個檔案的任何部分:將 JSON 指標附加到架構參考
  • #在目前架構的任何部分:將 JSON 指標附加到「#」

您也可以使用等於「#」的「$ref」來引用整個目前架構 - 它允許您建立引用自身的遞歸架構。

因此,在我們的範例中,navigation.json 中的架構指的是:

  • 架構 page.json
  • 架構中的定義 defs.json
  • 在同一架構中定義 positiveIntOrNull
#

page.json 中的架構指的是:

  • 返回架構 navigation.json
  • 還有 definitions 檔案中的 defs.json

該標準要求“$ref”應該是物件中的唯一屬性,因此如果除了另一個架構之外還想應用引用的架構,則必須使用 >“allOf”關鍵字。

任務 1

使用參考重構本教學第 1 部分的使用者架構。將架構分成兩個檔案:user.jsonconnection.json

將您的架構放入檔案part2/task1/user.jsonpart2/task1/connection.json 並執行node part2/ task1/validate 檢查您的架構是否正確。

JSON 指標

JSON 指標是定義 JSON 檔案各部分路徑的標準。該標準在 RFC6901 中進行了描述。

該路徑由用「/」字元連接的段(可以是任何字串)組成。如果段中包含字元“~”或“/”,則應將其替換為“~0”和“~1”。每個段表示 JSON 資料中的屬性或索引。

如果您查看導航範例,定義color 屬性的“$ref” 是“defs.json#/definitions/color”,其中“defs .json#”是模式URI,「/definitions/color」是JSON 指標。它指向屬性 definitions 內的屬性 color

慣例是將 refs 中使用的模式的所有部分放入模式的 definitions 屬性中(如範例所示)。儘管 JSON 模式標準為此目的保留了 definitions 關鍵字,但不需要將子模式放在那裡。 JSON 指標可讓您引用 JSON 檔案的任何部分。

當在 URI 中使用 JSON 指標時,所有在 URI 中無效的字元都應該被轉義(在 JavaScript 中可以使用全域函數 encodeURIComponent )。

JSON 指针不仅可以在 JSON 模式中使用。它们可用于表示 JSON 数据中任何属性或项目的路径。您可以使用库 json-pointer 来访问带有 JSON 指针的对象。

任务 2

下面的 JSON 文件描述了文件夹和文件结构(文件夹名称以“/”开头):

{
  "/": {
    "/documents": {
      "my_story~.rtf": {
        "type": "document",
        "application": ["Word", "TextEdit"],
        "size": 30476
      },
      ...
    },
    "/system": {
      "/applications": {
        "Word": {
          "type": "executable",
          "size": 1725058307
        },
        ...
      }
    }
  }
}
登入後複製

JSON 指针指向什么:

  • “Word”应用程序的大小,
  • “my_story~.rtf”文档的大小,
  • 可以打开“my_story~.rtf”文档的第二个应用程序的名称?

将您的答案放入 part2/task2/json_pointers.json 并运行 node part2/task2/validate 进行检查。

架构 ID

架构通常有一个顶级“id”属性,其中包含架构 URI。当在模式中使用“$ref”时,其值被视为相对于模式“id”解析的URI。

解析的工作方式与浏览器解析非绝对 URI 的方式相同 - 它们是相对于其 “id” 属性中的架构 URI 进行解析的。如果“$ref”是文件名,它将替换“id”中的文件名。在导航示例中,导航模式id为"http://mynet.com/schemas/navigation.json#",因此在引用"page.json#时" 已解析,页面架构的完整 URI 变为 "http://mynet.com/schemas/page.json#" (即“id” page.json 模式)。

如果页面架构的“$ref”是一个路径,例如"/page.json",那么它将被解析为 "http://mynet.com/page.json#"。并且 "/folder/page.json" 将被解析为 "http://mynet.com/folder/page.json#"

如果“$ref”从“#”字符开始,则将其视为哈希片段并附加到“id”中的路径(替换其中的哈希片段)。在导航示例中,引用 "defs.json#/definitions/color" 解析为 "http://mynet.com/schemas/defs.json# /definitions/color" 其中 "http://mynet.com/schemas/defs.json#" 是定义模式的 ID,"/definitions/ color" 在其内部被视为 JSON 指针。

如果“$ref”是具有不同域名的完整URI,就像链接在浏览器中的工作方式一样,它将被解析为相同的完整URI。

内部架构 ID

JSON 架构标准允许您在架构内使用 “id” 来标识这些子架构,并更改相对于内部引用进行解析的基本 URI - 这称为“更改解析”范围”。这可能是该标准中最令人困惑的部分之一,这就是为什么它不是很常用的原因。

我不建议过度使用内部 ID,但以下有一个例外,原因有二:

  • 很少有验证器在使用内部 ID 时始终遵循标准并正确解析引用(Ajv 在此完全遵循标准)。
  • 架构变得更加难以理解。

我们仍然会研究它的工作原理,因为您可能会遇到使用内部 ID 的架构,并且在某些情况下使用它们有助于构建您的架构。

首先,让我们看一下我们的导航示例。大多数引用都在 definitions 对象中,这使得引用相当长。有一种方法可以通过在定义中添加 ID 来缩短它们。这是更新后的 defs.json 架构:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/defs.json#",
  "title": "Definitions",  
  "definitions": {
    "positiveInteger": { "id": "#positiveInteger", "type": "integer", "minimum": 1 },
    "color": {
      "id": "#color",
      "anyOf": [
        { "enum": [ "red", "green", "blue", "white" ] },
        { "type": "string", "pattern": "^#(?:(?:[0-9a-fA-F]{1,2})){3}$" }
      ]
    }
  }
}
登入後複製

现在代替引用 "defs.json#/definitions/positiveInteger""defs.json#/definitions/color" 用于导航和页面架构,您可以使用较短的引用:"defs.json#positiveInteger""defs.json#color"。这是内部 ID 的一种非常常见的用法,因为它可以让您的参考文献更短、更易读。请注意,虽然大多数 JSON 模式验证器都可以正确处理这个简单的情况,但其中一些验证器可能不支持它。

让我们看一个更复杂的 ID 示例。以下是示例 JSON 架构:

{
  "id": "http://x.y.z/rootschema.json#",
  "definitions": {
    "bar": { "id": "#bar", "type": "string" }
  },
  "subschema": {
    "id": "http://somewhere.else/completely.json#",
    "definitions": {
      "bar": { "id": "#bar", "type": "integer" }
    },
    "type": "object",
    "properties": {
      "foo": { "$ref": "#bar" }
    }
  },
  "type": "object",
  "properties": {
    "bar": { "$ref": "#/subschema" },
    "baz": { "$ref": "#/subschema/properties/foo" },
    "bax": { "$ref": "http://somewhere.else/completely.json#bar" }
  }
}
登入後複製

在很少的几行内容中,它变得非常混乱。看一下示例并尝试找出哪个属性应该是字符串,哪个属性应该是整数。

该架构定义了一个具有属性 barbazbax 的对象。属性 bar 应该是根据子模式有效的对象,这要求其属性 foo 根据 "bar" 有效参考。因为子模式有自己的“id”,所以引用的完整 URI 将是 "http://somewhere.else/completely.json#bar",因此它应该是一个整数。

现在查看属性 bazbax。它们的引用以不同的方式编写,但它们指向相同的引用 "http://somewhere.else/completely.json#bar" 并且它们都应该是整数。尽管属性 baz 直接指向模式 { "$ref": "#bar" },但它仍然应该相对于子模式的 ID 进行解析,因为它就在里面。因此,根据此模式,下面的对象是有效的:

{
  "bar": { "foo": 1 },
  "baz": 2,
  "bax": 3
}
登入後複製

许多 JSON 模式验证器无法正确处理它,因此应谨慎使用更改解析范围的 ID。

任务 3

解决这个难题将帮助您更好地理解引用和更改分辨率范围的工作原理。您的架构是:

{
  "id": "http://x.y.z/rootschema.json#",
  "title": "Task 3",
  "description": "Schema with references - create a valid data",
  "definitions": {
    "my_data": { "id": "#my_data", "type": "integer" }
  },
  "schema1": {
    "id": "#foo",
    "allOf": [ { "$ref": "#my_data" } ]
  },
  "schema2": {
    "id": "otherschema.json",
    "definitions": {
      "my_data": { "id": "#my_data", "type": "string" }
    },
    "nested": {
      "id": "#bar",
      "allOf": [ { "$ref": "#my_data" } ]
    },
    "alsonested": {
      "id": "t/inner.json#baz",
      "definitions": {
        "my_data": { "id": "#my_data", "type": "boolean" }
      },
      "allOf": [ { "$ref": "#my_data" } ]
    }
  },
  "schema3": {
    "id": "http://somewhere.else/completely#",
    "definitions": {
      "my_data": { "id": "#my_data", "type": "null" }
    },
    "allOf": [ { "$ref": "#my_data" } ]
  },
  "type": "object",
  "properties": {
    "foo": { "$ref": "#foo" },
    "bar": { "$ref": "otherschema.json#bar" },
    "baz": { "$ref": "t/inner.json#baz" },
    "bax": { "$ref": "http://somewhere.else/completely#" },
    "quux": { "$ref": "#/schema3/allOf/0" }
  },
  "required": [ "foo", "bar", "baz", "bax", "quux" ]
}
登入後複製

创建一个根据此架构有效的对象。

将您的答案放入 part2/task3/valid_data.json 并运行 node part2/task3/validate 进行检查。

加载引用的架构

到目前为止,我们一直在研究相互引用的不同模式,而没有关注它们如何加载到验证器中。

一种方法是像上面的导航示例一样预加载所有连接的模式。但在某些情况下,它要么不切实际,要么不可能,例如,如果您需要使用的架构是由另一个应用程序提供的,或者您事先不知道可能需要的所有可能架构。

在这种情况下,验证器可以在验证数据时加载引用的架构。但这会使验证过程变慢。 Ajv 允许您将模式编译为验证函数,并异步加载流程中缺少的引用模式。验证本身仍然是同步且快速的。

例如,如果可以从其 ID 中的 URI 下载导航架构,则根据导航架构验证数据的代码可能如下:

var Ajv = require('ajv');
var request = require('request');
var ajv = Ajv({ allErrors: true, loadSchema: loadSchema });

var _validateNav; // validation function will be cached here once loaded and compiled

function validateNavigation(data, callback) {
  if (_validateNav) setTimeout(_validate);
  loadSchema('http://mynet.com/schemas/navigation.json', function(err, schema) {
    if (err) return callback(err);
    ajv.compileAsync(schema, function(err, v) {
      if (err) callback(err);
      else {
        _validateNav = v;
        _validate();
      }
    });
  });

  function _validate() {
    var valid = _validateNav(data);
    callback(null, { valid: valid, errors: _validateNav.errors });
  }
}


function loadSchema(uri, callback) {
  request.json(uri, function(err, res, body) {
    if (err || res.statusCode >= 400)
      callback(err || new Error('Loading error: ' + res.statusCode));
    else
      callback(null, body);
  });
}
登入後複製

代码定义了 validateNavigation 函数,该函数在第一次调用时加载架构并编译验证函数,并始终通过回调返回验证结果。有多种方法可以对其进行改进,从在第一次使用模式之前单独预加载和编译模式,到考虑到该函数在管理缓存模式之前可以多次调用这一事实(ajv.compileAsync 已确保架构始终仅请求一次)。

现在我们将了解为 JSON 架构标准第 5 版提议的新关键字。

JSON 架构版本 5 提案

尽管这些提案尚未最终确定为标准草案,但它们今天就可以使用——Ajv 验证器实现了它们。它们极大地扩展了您可以使用 JSON 模式验证的内容,因此值得使用它们。

要将所有这些关键字与 Ajv 一起使用,您需要使用选项 v5: true

关键字“常量”和“包含”

添加这些关键字是为了方便。

“constant”关键字要求数据等于关键字的值。如果没有此关键字,则可以通过元素数组中的一项的“enum”关键字来实现。

此架构要求数据等于 1:

{ "constant": 1 }
登入後複製

“contains” 关键字要求某些数组元素与该关键字中的架构相匹配。该关键字仅适用于数组;任何其他数据类型都将根据它有效。仅使用版本 4 中的关键字来表达此要求有点困难,但这是可能的。

此架构要求,如果数据是数组,则至少其中一项是整数:

{ "contains": { "type": "integer" } }
登入後複製

它相当于这个:

{
  "not": {
    "type": "array",
    "items": {
      "not": { "type": "integer" }
    }
  }
}
登入後複製

要使此模式有效,数据不应该是数组,或者它的所有项目都不应该是非整数(即某些项目应该是整数)。

请注意,如果数据是空数组,“contains” 关键字和上面的等效架构都会失败。

关键字“patternGroups”

建议将此关键字替换“patternProperties”。它允许您限制与对象中应存在的模式匹配的属性数量。 Ajv 在 v5 模式下同时支持 “patternGroups”“patternProperties”,因为第一个更加冗长,如果您不想限制属性的数量,可能更喜欢使用第二个。

例如架构:

{
  "patternGroups": {
    "^[a-z]+$": {
      "schema": { "type": "string" }
    },
    "^[0-9]+$": {
      "schema": { "type": "number" }
    }
  }
}
登入後複製

相当于这个模式:

{
  "patternProperties": {
    "^[a-z]+$": { "type": "string" },
    "^[0-9]+$": { "type": "number" }
  }
}
登入後複製

它们都要求对象仅具有以下属性:键仅包含小写字母且值类型为字符串,键值仅包含数字且值类型为数字。它们不需要任何数量的此类属性,也不限制最大数量。这就是您可以使用“patternGroups”执行的操作:

{
  "patternGroups": {
    "^[a-z]+$": {
      "minimum": 1,
      "maximum": 3,
      "schema": { "type": "string" }
    },
    "^[0-9]+$": {
      "minimum": 1,
      "schema": { "type": "number" }
    }
  }
}
登入後複製

上面的模式有额外的要求:至少应该有一个属性与每个模式匹配,并且键仅包含字母的属性不超过三个。

使用“patternProperties”无法实现相同的效果。

用于限制格式化值的关键字“formatMaximum” / “formatMaximum”

这些关键字与“exclusiveFormatMaximum” / “exclusiveFormatMinimum”一起允许您设置时间、日期和可能具有所需格式的其他字符串值的限制>“格式”关键字。

此架构要求数据是日期且大于或等于 2016 年 1 月 1 日:

{
  "format": "date",
  "formatMinimum": "2016-01-01"
}
登入後複製

Ajv 支持比较“日期”、“时间”和“日期-时间”格式的格式化数据,并且您可以定义支持 “formatMaximum” / 限制的自定义格式>“formatMaximum”关键字。

关键字“switch”

虽然之前的所有关键字要么允许您更好地表达没有它们的可能性,要么稍微扩展可能性,但它们并没有改变模式的声明性和静态性质。该关键字允许您使验证动态且依赖于数据。它包含多个 if-then 情况。

用一个例子更容易解释:

{
  "switch": [
    { "if": { "minimum": 50 }, "then": { "multipleOf": 5 } },
    { "if": { "minimum": 10 }, "then": { "multipleOf": 2 } },
    { "if": { "maximum": 4 }, "then": false }
  ]
}
登入後複製

上面的架构按“if”关键字中的子架构顺序验证数据,直到其中一个通过验证。当发生这种情况时,它会验证同一对象中的“then”关键字中的架构 - 这将是整个架构验证的结果。如果“then”的值为false,则验证立即失败。

这样,上面的模式要求值为:

  • 大于或等于 50 并且是 5 的倍数
  • 或 10 到 49 之间且 2 的倍数
  • 或 5 到 9 之间

这组特定的要求可以在没有 switch 关键字的情况下表达,但在更复杂的情况下这是不可能的。

任务 4

创建与上面最后一个示例等效的架构,而不使用 switch 关键字。

将您的答案放入 part2/task4/no_switch_schema.json 并运行 node part2/task4/validate 进行检查。

“switch”关键字案例还可以包含带有布尔值的“continue”关键字。如果此值为 true,则在成功的 “if” 模式匹配与成功的 “then” 模式验证后,验证将继续。这类似于 JavaScript switch 语句中的下一个情况,尽管在 JavaScript 中,fall-through 是默认行为,并且 “switch” 关键字需要显式的 “continue” 指令。这是另一个带有“继续”指令的简单示例:

"schema": {
  "switch": [
    { "if": { "minimum": 10 }, "then": { "multipleOf": 2 }, "continue": true },
    { "if": { "minimum": 20 }, "then": { "multipleOf": 5 } }
  ]
}
登入後複製

如果满足第一个“if”条件并且满足“then”要求,则验证将继续检查第二个条件。

“$data”参考

“$data” 关键字进一步扩展了 JSON 模式的可能性,并使验证更加动态和数据依赖。它允许您将某些数据属性、项目或键中的值放入某些架构关键字中。

例如,此模式定义了一个具有两个属性的对象,如果两个属性都定义了,“larger”应大于或等于“smaller”——“smaller”中的值用作“larger”的最小值:

"schema": {
  "properties": {
    "smaller": {},
    "larger": {
      "minimum": { "$data": "1/smaller" }
    }
  }
}
登入後複製

Ajv 为大多数值不是架构的关键字实现“$data”引用。如果“$data”引用指向不正确的类型,则验证失败;如果它指向未定义的值(或者对象中不存在路径),则验证成功。

那么“$data”引用中的字符串值是什么?它看起来与 JSON 指针类似,但又不完全一样。它是本标准草案定义的相对 JSON 指针。

它由一个整数组成,定义查找应遍历对象的次数(上例中的 1 表示直接父级),后跟“#”或 JSON 指针。

如果数字后跟“#”,则 JSON 指针解析的值将是属性的名称或对象具有的项目的索引。这样,“0#”代替“1/smaller”将解析为字符串“larger”,而“1#”将无效,因为整个数据不是任何对象或数组的成员。这个架构:

{
  "type": "object",
  "patternProperties": {
    "^date$|^time$": { "format": { "$data": "0#" } }
  }
}
登入後複製

相当于这个:

{
  "type": "object",
  "properties": {
    "date": { "format": "date" },
    "time": { "format": "time" }
  }
}
登入後複製

因为 { “$data”: “0#” } 被替换为属性名称。

如果指针中的数字后跟 JSON 指针,则从该数字引用的父对象开始解析此 JSON 指针。您可以在第一个“较小”/“较大”示例中看到它是如何工作的。

让我们再看看我们的导航示例。您可以在数据中看到的要求之一是页面对象中的 page_id 属性始终等于所包含的导航对象中的 parent_id 属性。我们可以使用 “$data” 引用在 page.json 模式中表达此要求:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/page.json#",
  ...
  "switch": [{
    "if": { "required": [ "navigation" ] },
    "then": {
      "properties": {
        "page_id": { "constant": { "$data": "1/navigation/parent_id" } }
      }
    }
  }]
}
登入後複製

添加到页面架构中的“switch”关键字要求,如果页面对象具有 navigation 属性,则 page_id 的值属性应与导航对象中 parent_id 属性的值相同。无需“switch”关键字也可以实现相同的效果,但它的表达能力较差并且包含重复:

{
  ...
  "anyOf": [
    { "not": { "required": [ "navigation" ] } },
    {
      "required": [ "navigation" ],
      "properties": {
        "page_id": { "constant": { "$data": "1/navigation/parent_id" } }
      }
    }
  ]
}
登入後複製

任务 5

相对 JSON 指针的示例可能会有所帮助。

使用 v5 关键字,使用两个必需属性 listorder 定义对象的架构。列表应该是一个最多包含五个数字的数组。所有项目都应该是数字,并且应按升序或降序排序,由属性 order 确定,可以是 "asc" “desc”

例如,这是一个有效的对象:

{
  "list": [ 1, 3, 3, 6, 9 ],
  "order": "asc"
}
登入後複製

这是无效的:

{
  "list": [ 9, 7, 3, 6, 2 ],
  "order": "desc"
}
登入後複製

将您的答案放入 part2/task5/schema.json 并运行 node part2/task5/validate 进行检查。

如何创建具有相同条件但大小不受限制的列表的架构?

定义新的验证关键字

我们研究了为 JSON 架构标准第 5 版提议的新关键字。您今天就可以使用它们,但有时您可能需要更多。如果您完成了任务 5,您可能已经注意到有些需求很难用 JSON 模式来表达。

一些验证器(包括 Ajv)允许您定义自定义关键字。自定义关键字:

  • 允许您创建无法使用 JSON-Schema 表达的验证场景
  • 简化您的架构
  • 帮助您将更大部分的验证逻辑引入您的架构
  • 使您的架构更具表现力、更简洁、更贴近您的应用领域

一位使用 Ajv 的开发人员在 GitHub 上写道:

> “具有自定义关键字的 ajv 在后端的业务逻辑验证方面为我们提供了很多帮助。我们使用自定义关键字将一大堆控制器级验证整合到 JSON 架构中。最终效果比编写单独的验证代码要好得多。”

在使用自定义关键字扩展 JSON 架构标准时,您必须注意的问题是架构的可移植性和理解性。您必须在其他平台上支持这些自定义关键字,并正确记录这些关键字,以便每个人都可以在您的架构中理解它们。

这里最好的方法是定义一个新的元架构,它将是草案 4 元架构或“v5 提案”元架构的扩展,其中将包括附加关键字的验证及其描述。然后,使用这些自定义关键字的架构必须将 $schema 属性设置为新元架构的 URI。

既然您已收到警告,我们将深入研究并使用 Ajv 定义几个自定义关键字。

Ajv 提供了四种定义自定义关键字的方法,您可以在文档中看到。我们将看看其中两个:

  • 使用将架构编译为验证函数的函数
  • 使用宏函数获取您的架构并返回另一个架构(带或不带自定义关键字)

让我们从范围关键字的简单示例开始。范围只是最小和最大关键字的组合,但如果您必须在架构中定义许多范围,特别是如果它们具有独占边界,则可能很容易变得无聊。

架构应该是这样的:

{
  "range": [5, 10],
  "exclusiveRange": true
}
登入後複製

当然,其中独占范围是可选的。定义该关键字的代码如下:

ajv.addKeyword('range', { type: 'number', compile: compileRange });
ajv.addKeyword('exclusiveRange'); // this is needed to reserve the keyword

function compileRange(schema, parentSchema) {
  var min = schema[0];
  var max = schema[1];

  return parentSchema.exclusiveRange === true
          ? function (data) { return data > min && data < max; }
          : function (data) { return data >= min && data <= max; }
}
登入後複製

就是这样!在此代码之后,您可以在架构中使用 range 关键字:

var schema = {
  "range": [5, 10],
  "exclusiveRange": true
};

var validate = ajv.compile(schema);

console.log(validate(5)); // false
console.log(validate(5.1)); // true
console.log(validate(9.9)); // true
console.log(validate(10)); // false
登入後複製

传递给addKeyword 的对象是一个关键字定义。它可以选择包含关键字适用的类型(或数组类型)。使用参数 schemaparentSchema 调用编译函数,并应返回另一个验证数据的函数。这使得它几乎与本机关键字一样高效,因为架构在编译期间进行了分析,但在验证期间存在额外函数调用的成本。

Ajv 允许您使用返回代码(作为字符串)的关键字来避免这种开销,该代码将成为验证函数的一部分,但它非常复杂,因此我们不会在这里讨论它。更简单的方法是使用宏关键字 - 您必须定义一个函数来获取该架构并返回另一个架构。

下面是 range 关键字的宏函数实现:

ajv.addKeyword('range', { type: 'number', macro: macroRange });

function macroRange(schema, parentSchema) {
  var resultSchema = {
    "minimum": schema[0],
    "maximum": schema[1]
  };

  if (parentSchema.exclusiveRange === true) {
    resultSchema.exclusiveMimimum = resultSchema.exclusiveMaximum = true;
  }

  return resultSchema;
}
登入後複製

您可以看到该函数只是返回与 range 关键字等效的新架构,该关键字使用关键字 maximumminimum。< /p>

让我们看看如何创建一个包含 range 关键字的元架构。我们将使用草案 4 元架构作为起点:

{
  "id": "http://mynet.com/schemas/meta-schema-with.range.json#",
  "$schema": "http://json-schema.org/draft-04/schema#",
  "allOf": [
    { "$ref": "http://json-schema.org/draft-04/schema#" },
    {
      "properties": {
        "range": {
          "description": "1st item is minimum, 2nd is maximum",
          "type": "array",
          "items": [ { "type": "number" }, { "type": "number" } ],
          "additionalItems": false 
        },
        "exclusiveRange": {
          "type": "boolean",
          "default": false
        }
      },
      "dependencies": {
        "exclusiveRange": [ "range" ]
      } 
    }
  ]
}
登入後複製

如果您想将 “$data” 引用与 range 关键字一起使用,则必须扩展 Ajv 中包含的“v5proposal”元模式(请参阅上面的链接),以便这些引用可以是 rangeexclusiveRange 的值。虽然我们的第一个实现不支持 “$data” 引用,但具有宏功能的第二个实现将支持它们。

现在您已经有了元架构,您需要将其添加到 Ajv 并使用 range 关键字在架构中使用它:

ajv.addMetaSchema(require('./meta-schema-with-range.json'));

var schema = {
  "$schema": "http://mynet.com/schemas/meta-schema-with-range.json#",
  "range": [5, 10],
  "exclusiveRange": true
};

var validate = ajv.compile(schema);
登入後複製

如果将无效值传递给 rangeexclusiveRange,上面的代码将引发异常。

任务 6

假设您已经定义了关键字 jsonPointers ,它将架构应用于由 JSON 指针定义的深层属性,这些属性指向从当前指针开始的数据。此关键字与 switch 关键字一起使用非常有用,因为它允许您定义深层属性和项目的要求。例如,此架构使用 jsonPointers 关键字:

{
  "jsonPointers": {
    "0/books/2/title": { "pattern": "json|Json|JSON" },
  }
}
登入後複製

相当于:

  {
    "properties": {
      "books": {
        "items": [
          {},
          {},
          {
            "properties": {
              "title": { "pattern": "json|Json|JSON" }
            }
          }
        ]
      }
    }
  }
登入後複製

假设您还定义了关键字 requiredJsonPointers ,其工作方式与 required 类似,但使用 JSON 指针而不是属性。

如果你愿意,你也可以自己定义这些关键字,或者你可以在文件 part2/task6/json_pointers.js 中查看它们是如何定义的。

您的任务是:使用关键字 jsonPointersrequiredJsonPointers,定义类似于 JavaScript select >switch 语句并具有以下语法(否则 fallthrough 是可选的):

{
  "select": {
    "selector": "",
    "cases": [
      { "case": , "schema": {  }, "fallthrough": true },
      { "case": , "schema": {  } },
      ...
    ],
    "otherwise": {  }
  }
}
登入後複製

此语法允许任何类型的值。请注意,fallthroughswitch 关键字中的 switch 不同。 fallthrough 将下一个案例的模式应用于数据,而不检查选择器是否等于下一个案例的值(因为它很可能不相等)。

将您的答案放入 part2/task6/select_keyword.jspart2/task6/v5-meta-with-select.json 并运行 节点part2/task6/validate来检查它们。

奖励 1:改进您的实现以支持此语法:

{
  "select": {
    "selector": "",
    "cases": {
      "": {  },
      "": {  },
      ...
    },
    "otherwise": {  }
  }
}
登入後複製

如果所有值都是不同的字符串并且没有 fallthrough 则可以使用。

奖励 2:扩展“v5 提案”元架构以包含此关键字。

JSON 模式的其他用法

除了验证数据之外,JSON 模式还可用于:

  • 生成用户界面
  • 生成数据
  • 修改数据

如果您有兴趣,可以查看生成 UI 和数据的库。我们不会探讨它,因为它超出了本教程的范围。

我们将考虑使用 JSON 模式在验证数据时修改数据。

过滤数据

验证数据时的常见任务之一是从数据中删除附加属性。这允许您在将数据传递到处理逻辑之前清理数据,而不会导致模式验证失败:

var ajv = Ajv({ removeAdditional: true });

var schema = {
  "type": "object",
  "properties": {
    "foo": { "type": "string" }
  },
  "additionalProperties": false
};

var validate = ajv.compile(schema);

var data: { foo: 1, bar: 2 };

console.log(validate(data)); // true
console.log(data); // { foo: 1 };
登入後複製

如果没有选项 removeAdditional,验证将会失败,因为模式不允许有一个附加属性 bar。使用此选项,验证将通过并且该属性将从对象中删除。

removeAdditional 选项值为 true 时,仅当 additionalProperties 关键字为 false 时才会删除附加属性。 Ajv 还允许您删除所有附加属性,无论 additionalProperties 关键字或验证失败的附加属性(如果 additionalProperties 关键字是架构)。请查看 Ajv 文档以获取更多信息。

为属性和项指定默认值

JSON 架构标准定义了关键字“default”,该关键字包含数据应具有的值(如果未在验证数据中定义该值)。 Ajv 允许您在验证过程中分配此类默认值:

var ajv = Ajv({ useDefaults: true });

var schema = {
  "type": "object",
  "properties": {
    "foo": { "type": "number" },
    "bar": { "type": "string", "default": "baz" }
  },
  "required": [ "foo", "bar" ]
};

var data = { "foo": 1 };

var validate = ajv.compile(schema);

console.log(validate(data)); // true
console.log(data); // { "foo": 1, "bar": "baz" }
登入後複製

如果没有选项 useDefaults,验证将会失败,因为验证的对象中没有必需的属性 bar 。使用此选项,验证将通过,并且具有默认值的属性将添加到对象中。

强制数据类型

“type” 是 JSON 模式中最常用的关键字之一。当您验证用户输入时,从表单获取的所有数据属性通常都是字符串。 Ajv 允许您将数据强制为架构中指定的类型,以便通过验证并在之后使用正确类型的数据:

var ajv = Ajv({ coerceTypes: true });
var schema = {
  "type": "object",
  "properties": {
    "foo": { "type": "number" },
    "bar": { "type": "boolean" }
  },
  "required": [ "foo", "bar" ]
};

var data = { "foo": "1", "bar": "false" };

var validate = ajv.compile(schema);

console.log(validate(data)); // true
console.log(data); // { "foo": 1, "bar": false }
登入後複製

比较 JavaScript JSON 模式验证器

有十多个可用的积极支持的 JavaScript 验证器。您应该使用哪一个?

您可以在项目 json-schema-benchmark 中查看性能基准以及不同验证器如何通过 JSON-schema 标准的测试套件。

一些验证器还具有独特的功能,可以使它们最适合您的项目。我将在下面比较其中一些。

is-my-json-valid 和 jsen

这两个验证器速度非常快并且接口非常简单。它们都将模式编译为 JavaScript 函数,就像 Ajv 所做的那样。

它们的缺点是它们对远程引用的支持都很有限。

图式龙

这是一个独一无二的库,其中 JSON 模式验证几乎是一种副作用。

它被构建为通用且易于扩展的 JSON 模式处理器/迭代器,您可以使用它来构建使用 JSON 模式的各种工具:UI 生成器、模板等。

它已经包含了相对快速的 JSON 模式验证器。

不过,它根本不支持远程引用。

忒弥斯

它是快速验证器组中最慢的,它具有一套全面的功能,但对远程引用的支持有限。

它真正的亮点是它对 default 关键字的实现。虽然大多数验证器对此关键字的支持有限(Ajv 也不例外),但 Themis 具有非常复杂的逻辑,即在复合关键字(例如 anyOf)内应用默认值和回滚。

z 架构

就性能而言,这个非常成熟的验证器处于快速验证器和慢速验证器之间的边界。在新型编译验证器出现之前,它可能是最快的验证器之一(以上所有内容和 Ajv)。

它通过了验证器 JSON 模式测试套件中的几乎所有测试,并且对远程引用的实现相当彻底。

它有大量选项,允许您调整许多 JSON 模式关键字的默认行为(例如,不接受空数组作为数组或空字符串集)并对 JSON 模式施加额外要求(例如,需要minLength 关键字(字符串)。

我认为在大多数情况下,修改架构行为以及在 JSON 架构中包含对其他服务的请求都是错误的做法。但在某些情况下,这样做的能力会大大简化。

电视4

这是支持该标准第 4 版的最古老(也是最慢)的验证器之一。因此,它通常是许多项目的默认选择。

如果您正在使用它,了解它如何报告错误和丢失引用并正确配置它非常重要,否则您将收到许多误报(即,验证通过时包含无效数据或未解析的远程引用) .

默认情况下不包含格式,但它们可以作为单独的库提供。

Ajv

我编写 Ajv 是因为所有现有验证器要么速度快,要么符合标准(特别是在支持远程引用方面),但不是两者兼而有之。 Ajv 填补了这一空白。

目前它是唯一的验证器:

  • 通过所有测试并完全支持远程引用
  • 支持为标准版本 5 和 $data 参考建议的验证关键字
  • 支持自定义格式和关键字的异步验证

它具有修改验证过程和修改验证数据的选项(过滤、分配默认值和强制类型 - 请参阅上面的示例)。

使用哪个验证器?

我认为最好的方法是尝试多种方法并选择最适合您的方法。

我编写了 json-schema-consolidate,它提供了一组适配器,统一了 12 个 JSON 模式验证器的接口。使用此工具,您可以减少在验证器之间切换的时间。我建议您在决定使用哪个验证器后将其删除,因为保留它会对性能产生负面影响。

就是这个了!我希望本教程有用。您已了解:

  • 建構架構
  • 使用參考文獻和 ID
  • 使用驗證關鍵字和版本 5 提案中的 $data 參考
  • 非同步載入遠端架構
  • 定義自訂關鍵字
  • 在驗證過程中修改資料
  • 不同 JSON 架構驗證器的優缺點

感謝您的閱讀!

以上是繼續驗證資料:使用JSON-Schema進行驗證,第二部分的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!