kickflow Tech Blog

株式会社kickflowの開発チームによるブログ

「JSON Schemaを知っている」人のための今どきJSON Schemaの紹介記事

ナヌークサウルスの復元図
最近はティラノサウルス科恐竜には羽毛があったとするのが定説(本文には関係ありません)

こんにちは。株式会社kickflow・プロダクト開発本部の小本です。

最近、「設定をJSONファイルとしてエクスポート・インポートする機能」を実装する際にJSON Schemaをウン年ぶりに使いました。その際、私のJSON Schema観がジュラ紀のまま止まっていたことに気づいたので、最新のJSON Schemaにどんな機能が追加されているか、触りだけ紹介します。

JSON Schemaとは?

JSON Schema(公式サイト)はJSONデータの構造を定義するための規格です。

  • JSONのフォーマットを人間 / 機械が読める形式で定義できる
  • JSONデータがスキーマに沿っているか自動でチェックできる
  • 複数の言語で実装されているので、一度スキーマを書けば、フロントエンドとサーバサイドなどで使いまわせる

といった特徴があります。

例えばOpenAPI Specification(a.k.a Swagger Specification)のコンポーネント定義はJSON Schemaを採用しています。

例:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/product.schema.json",
  "title": "Product",
  "description": "A product from Acme's catalog",
  "type": "object",
  "properties": {
    "productId": {
      "description": "The unique identifier for a product",
      "type": "integer"
    },
    "productName": {
      "description": "Name of the product",
      "type": "string"
    }
  }
}

このスキーマは、以下の「OK」のようなJSONデータを定義しており、「NG」のように型が違う場合はバリデーションエラーにできます。

OK

{
  "productId": 1,
  "productName": "A green door",
}

NG

{
  "productId": "foo",
  "productName": { "name": "A green door" },
}

ここで ああ、JSON Schemaね、知ってる知ってると思った人は、JSON Schemaにこんな印象を持っているかも知れません

  • 結局、文字列の中身は自前でチェックしないといけないんだよね
  • 同じ内容を何度も書かなきゃならない、DRYじゃない
  • スキーマファイルが巨大化して収集がつかなくなる
  • 場合分けが要るケースは無理

あなたのその認識、ジュラ紀ですよ。

その前に:そのライブラリは古い ⚠️

例えばRuby on Rails製アプリでOpenAPIライブラリのcomittee を使っていると、推移的依存により json_schema というライブラリもインストールされるはずです。そのためJSONデータのチェックが必要になったとき、

「なんだ、json_schemaってライブラリがもう入ってるじゃん。これを使えばいいや!」

と思うかも知れません。残念! json_schemaは、Draft-4という古いJSON Schema規格にしか対応していないのです。

最新規格に対応したライブラリは公式サイトのImplementationページにリストアップされています。

Rubyの場合はJSONSchemerというライブラリです。

文字列の中身をチェック①:"format":

"format": で、文字列の中身が"date"(日付)や"email"(メールアドレス)といった、所定のフォーマットであることを宣言できます。 定義済みのフォーマットは "date-time", "time", "hostname", "uri", "ipv4", "ipv6", "uuid" ... など、よく使うものは網羅されています。 もちろん、自前で追加も可能です。

また、正規表現でチェックする"pattern":もあります。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "birthday": {
      "type": "string",
      "format": "date"
    }
  }
}

OK

{
  "birthday": "1990-12-15"
}

NG

{
  "birthday": "1990-12-32"
}

文字列の中身をチェック②:"enum":

「ステータス」「性別」など、値が有限個の場合には、文字列の代わりに "enum": を使えます。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "status": {
      "enum": [
        "draft",
        "in_progress"
      ]
    }
  }
}

OK

{
  "status": "draft"
}

NG

{
  "status": "completed"
}

重複の除去:"$ref:""$defs":

"$defs":でサブスキーマを定義し、"$ref:" で参照します。これにより繰り返しをDRYにしたり、巨大すぎるスキーマを分割することができます。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "author": {
      "$ref": "#/$defs/user"
    },
    "approver": {
      "$ref": "#/$defs/user"
    }
  },
  "$defs": {
    "user": {
      "type": "object",
      "required": [
        "firstName",
        "lastName"
      ],
      "properties": {
        "firstName": {
          "type": "string"
        },
        "lastName": {
          "type": "string"
        }
      }
    }
  }
}

スキーマファイルの分割

"$ref:""$defs":を使ってもなおスキーマが巨大すぎるという場合には、スキーマファイル自体を分割できます。

設定方法はライブラリごとに異なりますが、大体どれも簡単にできるはずです。

resolve = lambda do |uri|
  path = uri.path[1..]
  JSON.parse(File.read(path))
end

schemer = JSONSchemer.schema(schema, ref_resolver: resolve)
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "author": {
      "$ref": "user.json"
    },
    "approver": {
      "$ref": "user.json"
    }
  }
}

また、"$ref": には外部のURLを指定することもできます。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "author": {
      "$ref": "https://example.com/schemas/address"
    },
    "approver": {
      "$ref": "https://example.com/schemas/address"
    }
  }
}

条件分岐: "if": / "then":"allOf":

現在のJSON Schemaでは条件分岐も表現できます。

ここで「スキーマ定義言語でどうやって条件式を表現するんだ?DSLがあるのか?」と思うかも知れません。「"if": で指定したスキーマに合致したら、"then":のスキーマにも合致しなければならない("if":に合致しない場合は単に無視される)」という形で表現します。

そして"allOf":は「"allOf":に指定したスキーマ全てに合致すること」を宣言するので、"if": / "then":"allOf": を組み合わせて、プログラミング言語でいう switch文のような内容も以下のように書けます。

なお(もちろん)"else":"anyOf":(OR)、"oneOf":(XOR)、 "not": (NOT)も提供されています。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$comment": "notificationType=userならemailが必須、notificationType=teamならteamCodeincludeSubteamが必須になる"
  "properties": {
    "kind": {
      "enum": [
        "user",
        "team"
      ]
    },
    "email": {
      "type": "string",
      "format": "email"
    },
    "teamCode": {
      "type": "string"
    },
    "includeSubteam": {
      "type": "boolean"
    },
    "allOf": [
      {
        "if": {
          "notificationType": {
            "const": "user"
          },
          "then": {
            "required": ["email"]
          }
        }
      },
      {
        "if": {
          "notificationType": {
            "const": "team"
          },
          "then": {
            "required": ["teamCode", "includeSubteam"]
          }
        }
      }
    ]
  }
}

最新のJSON Schemaを知るには?

公式サイトのDocページにすべての文法が網羅されています。

各プログラミング言語のライブラリは公式サイトのImplementationページにリストアップされています。

We are hiring!

kickflow(キックフロー)は、運用・メンテナンスの課題を解決する「圧倒的に使いやすい」クラウドワークフローです。

kickflow.com

サービスを開発・運用する仲間を募集しています。株式会社kickflowはソフトウェアエンジニアリングの力で社会の課題をどんどん解決していく会社です。こうした仕事に楽しさとやりがいを感じるという方は、カジュアル面談、ご応募お待ちしています!

careers.kickflow.co.jp