こんにちは、黒羽です。
ここしばらく開発しているアプリケーションでは、GUI画面の入力項目やブラウザからWebSocketのメッセージとして渡されたパラメータをJsonSchemaファイルに定義した条件でバリデーションするという実装を行ったので定義ファイルを作成するときに詰まったポイントなどを共有します。
そもそも
JsonSchemaってなんやねんってときはググればだいたい解決できる世の中ですが、ものすごくざっくりというとプロパティの検証に必要な条件をJson形式で記述したスキーマ言語です。
以下のJsonはかの有名な「見た目は子供、頭脳は大人」な某小学生のプロフィールを基に書いてみました。
{
"name": "江戸川コナン",
"age": 6,
"birthdayMonth": "May",
"birthdayDay": 4,
"sex": "male",
"job": ["student",
"detective"
]
}
この情報をベースに某小学生の通う小学校に入ることのできる人物を定義してみました。
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "http://example.com/example.json",
"type": "object",
"default": {},
"required": [
"name",
"age",
"birthdayMonth",
"birthdayDay",
"sex",
"job"
],
"properties": {
"name": {
"type": "string",
"minLength": 1,
},
"age": {
"type": "integer",
"default": 0,
"minValue": 5
},
"birthdayMonth": {
"type": "string",
"enum": [ "Jan", "Feb", "Mar", "Apr", "May","Jun", "Jul", "Aug"
, "Sep", "Oct", "Nov", "Dec"
]
},
"birthdayDay": {
"type": "integer",
"default": 1,
"maxValue": 31,
"minValue": 1
},
"sex": {
"type": "string",
"default": "",
"enum": [
"male","female"
]
},
"job": {
"type": "array",
"default": [],
"items": {
"type": "string",
"enum": [
"student",
"detective",
"parent"
]
}
}
}
}
定義した条件としては
- 名前を1文字以上持っていること
- 5歳以上であること
- 誕生月の略称がenumで定義した項目に含まれること
- 誕生日が1~31の範囲であること
- 性別が男女のどちらかであること
- 職業が学生、探偵、保護者のいずれかであること
となります。
主題から脱線しそうなのでこれ以上の深掘りはしませんが某黒ずくめの組織の長髪の兄貴だとか警察関係者は立ち入り禁止、眠りの名探偵は職業が探偵、保護者なので立ち入り許可が出ます。
基本的なデータ型のチェック(type)、数字型の値の範囲の判定(minValue, maxValue)、リスト内の項目合致(enum)と基本的なものであれば上記の定義だけで十分です。
if分岐なども可能なので条件によって子プロパティが動的に変わるプロパティなども定義はできますがかなり記述量が増えます。
ここからは不便な点、詰まった点を共有します。
判定の結果がエラーだった場合において、エラー理由を取得すると日本語以外のメッセージしか取得できない
現状、JsonSchemaのエラーメッセージは日本語に対応しておらず、英語含めて3か国語程度しか選択できないようです。
日本国内での使用が一番想定されている開発中のアプリではユーザーに英語のエラーメッセージを解読してもらうのは忍びないのでJsonSchemaに用意されている予約語以外のフィールド名を使用するとAnnotationsというフィールドにオブジェクトとしてまとめられる仕様を利用して日本語メッセージを返すようにしています。
動的なプロパティを作ろうとすると記述量が膨大になる
if分岐があると上で書きましたがif-thenをJson形式で書くため、素直に書くと記述量がとんでもないことになります。
今回のアプリではcase文のような分岐が必要だったため以下のような雰囲気で記述しました。
"allOf": [
{
"if": {
"properties": {
"job": {"const": "student"}
}
},
"then": {
"properties": {
"details": { "$ref": "#/definitions/student" }
}
}
},
{
"if": {
"properties": {
"job": { "const": "detective" }
}
},
"then": {
"properties": {
"details": { "$ref": "#/definitions/detective" }
}
}
}
]
見慣れないプロパティはドキュメント等を参照してもらうとして、条件としては「jobが”student”だった場合はdefinitionsプロパティに定義したstudentのdetailsプロパティを参照し、jobが”detective”だった場合はdetectiveのdetailsプロパティを参照する、それ以外はdetailsを持たない」となっています。
allOfのおかげでここは比較的に簡潔に書けました。(それでもdefinitions項目に別途色々定義したりする必要があるのでJson自体はカオス。)
Formatのチェックがあるがかなり甘い
文字列が特定の形式にのっとっているかを判定してくれるFormatフィールドがあり、URIやIPアドレスの形式チェックができます。これは便利!!!と思って使っていますが細かいところでNGパターンがチェックからすり抜けます。止む無し。
試した過程であったのは
- IPアドレス→第4オクテットがなくても正常と判断される
- URI→「:/」でも正常と判断される
といったものです。他のサイトを調べても「Formatだけじゃ不十分だからPatternできちんと正規表現で潰してね!」みたいな感じだったのでFormatとPatternの併用で潰している箇所があります。(ポート番号はさすがにないけど最大値最小値で判定するのは微妙だったのでここも正規表現を使って潰していたり。)
終わりに
今回はJsonSchemaについてでした。
入力値の簡単なチェック処理をソースコードにだらっと書くのが嫌い、美しくない!と思う方は試してみると面白いかもしれないです。