みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

JavaScriptでJSON SchemaとMeta-schemaを使ってみる

JSON Schema を使えば,自分が欲しい構造の JSON データであるかを簡単に判定できるので,とても便利です.

まずは JavaScript で JSON Schema を使ってみます.

ajv を使う

JSON schema バリデータの実装はたくさんありますが,今回は,Ajv JSON schema validator を使って,以下のようなスクリプトを実行してみます.

package.json

{
  "dependencies": {
    "ajv": "^8.12.0"
  }
}

main.mjs

import Ajv from "ajv";

const ajv = new Ajv(); // draft-07

const schema = {
    type: "object",
    properties: {
        a: { type: "integer" },
        b: { type: "string" },
    },
    required: ["a"],
    additionalProperties: false,
}

const data = {
    a: 1,
    b: 2,
}

const validate = ajv.compile(schema);
const valid = validate(data);
console.log(valid);

schema をコンパイルして data を検証するわけです.これを実行すると,b プロパティが string ではなく integer になっているので,false と出力されます.

const data = {
    a: 1,
    b: "Banana", // fix
}

これで true を出力されます.

JSON schema を作成

JSON Schema - Creating your first schema

上記の記事にまとまっています.要約すると type, properties をそれっぽく指定していくだけです.

JSON の構造を JSON で記述できるので面白いです.

JSON Schema を検証

任意の JSON Schema を検証できる JSON Schema があれば便利だと思って調べたところ,Meta-schema という名前で用意されていました.

"draft-2020-12" を使用して "draft-07" の Mata-schema をコンパイルし,先ほどの JSON Schema を検証してみましょう.なお,本当は "draft-07" を使用して "draft-07" をコンパイルしたかったのですが,ajv に重複する Meta-schema を登録できなかったので,諦めました.

main.mjs

import Ajv2020 from "ajv/dist/2020.js";

const ajv = new Ajv2020({ strict: false });

const schema = {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "http://json-schema.org/draft-07/schema#",
    "title": "Core schema meta-schema",
    "definitions": {
        "schemaArray": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#" }
        },
        "nonNegativeInteger": {
            "type": "integer",
            "minimum": 0
        },
        "nonNegativeIntegerDefault0": {
            "allOf": [{ "$ref": "#/definitions/nonNegativeInteger" }, { "default": 0 }]
        },
        "simpleTypes": {
            "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
        },
        "stringArray": {
            "type": "array",
            "items": { "type": "string" },
            "uniqueItems": true,
            "default": []
        }
    },
    "type": ["object", "boolean"],
    "properties": {
        "$id": {
            "type": "string",
            "format": "uri-reference"
        },
        "$schema": {
            "type": "string",
            "format": "uri"
        },
        "$ref": {
            "type": "string",
            "format": "uri-reference"
        },
        "$comment": {
            "type": "string"
        },
        "title": {
            "type": "string"
        },
        "description": {
            "type": "string"
        },
        "default": true,
        "readOnly": {
            "type": "boolean",
            "default": false
        },
        "examples": {
            "type": "array",
            "items": true
        },
        "multipleOf": {
            "type": "number",
            "exclusiveMinimum": 0
        },
        "maximum": {
            "type": "number"
        },
        "exclusiveMaximum": {
            "type": "number"
        },
        "minimum": {
            "type": "number"
        },
        "exclusiveMinimum": {
            "type": "number"
        },
        "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
        "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
        "pattern": {
            "type": "string",
            "format": "regex"
        },
        "additionalItems": { "$ref": "#" },
        "items": {
            "anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/schemaArray" }],
            "default": true
        },
        "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
        "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
        "uniqueItems": {
            "type": "boolean",
            "default": false
        },
        "contains": { "$ref": "#" },
        "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
        "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
        "required": { "$ref": "#/definitions/stringArray" },
        "additionalProperties": { "$ref": "#" },
        "definitions": {
            "type": "object",
            "additionalProperties": { "$ref": "#" },
            "default": {}
        },
        "properties": {
            "type": "object",
            "additionalProperties": { "$ref": "#" },
            "default": {}
        },
        "patternProperties": {
            "type": "object",
            "additionalProperties": { "$ref": "#" },
            "propertyNames": { "format": "regex" },
            "default": {}
        },
        "dependencies": {
            "type": "object",
            "additionalProperties": {
                "anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/stringArray" }]
            }
        },
        "propertyNames": { "$ref": "#" },
        "const": true,
        "enum": {
            "type": "array",
            "items": true,
            "minItems": 1,
            "uniqueItems": true
        },
        "type": {
            "anyOf": [
                { "$ref": "#/definitions/simpleTypes" },
                {
                    "type": "array",
                    "items": { "$ref": "#/definitions/simpleTypes" },
                    "minItems": 1,
                    "uniqueItems": true
                }
            ]
        },
        "format": { "type": "string" },
        "contentMediaType": { "type": "string" },
        "contentEncoding": { "type": "string" },
        "if": { "$ref": "#" },
        "then": { "$ref": "#" },
        "else": { "$ref": "#" },
        "allOf": { "$ref": "#/definitions/schemaArray" },
        "anyOf": { "$ref": "#/definitions/schemaArray" },
        "oneOf": { "$ref": "#/definitions/schemaArray" },
        "not": { "$ref": "#" }
    },
    "default": true
}

const data = {
    type: "object",
    properties: {
        a: { type: "integer" },
        b: { type: "string" },
    },
    required: ["a"],
    additionalProperties: false,
}
const validate = ajv.compile(schema);
const valid = validate(data);
console.log(valid);

true と出力されました.Meta-schema は自分自身と全ての JSON Schema を検証可能な JSON Schema であり,JSON Schema は任意の JSON を検証できるので,全ての JSON は検証可能になっているというわけです.

日常で使う Meta-schema

ajv に用意されている,ajv.validateSchema(schema: object): boolean を使用します.この関数は schema が追加された時に自動で呼び出されるので,ajv.compile(schema) を実行したら,meta-schema による検証が行われるようになっています*1.したがって,以下の2行だけを記載すれば十分です.

const validate = ajv.compile(schema);
const valid = validate(data);