Rustで$refを使うJSON Schemaを作成してバリデーションする

次のようなJSON Schemaがあるとする。

{
  "type": "object",
  "properties": {
    "foo": {
      "$ref": "#/definitions/foo"
    }
  },
  "required": [
    "foo"
  ]
}

$refでJSON Pointer "#/definitions/foo"によって参照しているスキーマは内部に存在してほしいが、なぜか今は別の場所にあるとする。構造は次のような形。

{
  "foo": {
    "properties": {
      "bar": {
        "type": "string"
      },
      "baz": {
        "type": "integer"
      }
    },
    "required": [
      "bar",
      "baz"
    ],
    "type": "object"
  }
}

このサブスキーマを本体スキーマとマージして、$refを通じて解決し、バリデーションに使いたい。

このようなときはserde_jsonjsonschemaを使って1次の手順で実装できる。

ここで、as_object_mutValueがJSON Schemaオブジェクト(プロパティを持つ型)になっている必要があるので、今は常にスキーマ本体がオブジェクト型である前提とする。

コードは次のようになる。

fn main() {
    // スキーマ本体
    let mut schema = serde_json::json!({
        "type": "object",
        "properties": {
            "foo": {
                "$ref": "#/definitions/foo"
            }
        },
        "required": ["foo"]
    });

    // 他の箇所から取得してきたサブスキーマ
    let definitions = serde_json::json!({
        "foo": {
            "type": "object",
            "properties": {
                "bar": {
                    "type": "string",
                },
                "baz": {
                    "type": "integer",
                }
            },
            "required": ["bar", "baz"],
        },
    });

    // スキーマ本体にサブスキーマを追加
    schema
        .as_object_mut()
        .expect("schema should be an object")
        .insert("definitions".to_string(), definitions);

    println!(
        "{}",
        serde_json::to_string_pretty(&schema).expect("schema should be serializable")
    );

    // スキーマのコンパイル時にJSON Pointerでサブスキーマを参照している$refを解決
    let compiled = jsonschema::JSONSchema::options()
        .compile(&schema)
        .expect("schema should be valid");

    // JSONのバリデーション
    let valid_instance = serde_json::json!({"foo": {"bar": "hello", "baz": 42}});
    println!("{}のバリデーション", valid_instance);
    validate(&compiled, valid_instance);

    let invalid_instance = serde_json::json!({"foo": {"bar": "hello"}});
    println!("{}のバリデーション", invalid_instance);
    validate(&compiled, invalid_instance);
}

fn validate(schema: &jsonschema::JSONSchema, instance: serde_json::Value) {
    match schema.validate(&instance) {
        Ok(_) => println!("instance is valid"),
        Err(errors) => {
            for error in errors {
                println!("validation error: {}", error);
                println!("instance path: {}", error.instance_path);
            }
        }
    };
}
# 実行結果
{
  "definitions": {
    "foo": {
      "properties": {
        "bar": {
          "type": "string"
        },
        "baz": {
          "type": "integer"
        }
      },
      "required": [
        "bar",
        "baz"
      ],
      "type": "object"
    }
  },
  "properties": {
    "foo": {
      "$ref": "#/definitions/foo"
    }
  },
  "required": [
    "foo"
  ],
  "type": "object"
}
{"foo":{"bar":"hello","baz":42}}のバリデーション
instance is valid
{"foo":{"bar":"hello"}}のバリデーション
validation error: "baz" is a required property
instance path: /foo

jsonschemaでは内部スキーマを指す$refを適切に解決するResolverを持っているので、JSONSchema::compileでスキーマをコンパイルするときに$refも自動的に解決される。


  1. serde_json 1.0.108, jsonschema 0.17.1を利用した