
目次
はじめに
こんにちは、開発本部開発1部の あかがわまさとも です。
2026年2月21日に開催された Go Conference mini in Sendai 2026 にて、「google/jsonschema-goのこれまでとこれから」というタイトルで登壇させていただきました。本記事では、調査の過程で行った、Go の JSON Schema ライブラリたちが、それぞれどのように JSON Schema を表現しているかの比較について述べます。
当日の発表では、JSON Schemaのおさらいから、google/jsonschema-go の登場背景、機能についてお話ししました。よければご覧ください。
JSON Schema について
JSON Schema は、JSONデータの構造や型、制約を定義するための言語です。JSON Schema それ自体もJSONで書かれます。
{ "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer", "minimum": 0 } }, "required": ["name"] }
上に挙げた例はシンプルに見えますが、JSON Schema を Go のデータ構造で表現しようとすると、いくつかの難しさがあります。
キーワードの多さ: JSON Schema のキーワードは type、properties、items、minimum、maximum、pattern、allOf、anyOf、$ref など非常に多岐にわたります。これらをまとめて Go の型、例えば構造体として表現するには、多くのフィールドを用意する必要があります。そして、付随する機能たちの実装も比例して大きくなります。
バージョン間のキーワード差分: JSON Schema にはいくつかのドラフトバージョンがあり、現在は Draft 2020-12 が最新です。バージョン間ではキーワード名やセマンティクスが変わることがあります。例えば、スキーマ定義の格納先は Draft 7 では definitions ですが Draft 2020-12 では $defs です。タプルのバリデーションは Draft 7 では items(配列形式)ですが Draft 2020-12 では prefixItems に変わりました。同じデータ構造で複数バージョンをどう扱うかは設計上の判断が分かれるところです。
$ref による参照: JSON Schema ではスキーマ同士が $ref で参照し合います。この参照を Go のデータ構造上でどう保持するかも、ライブラリの設計思想が現れるポイントです。
ライブラリごとの JSON Schemaの表現の比較
今回は著名さと自分の興味から、以下の4つを比較対象としました。
- google/jsonschema-go
- reference
- 元々 Go の公式 MCP SDK 内で作られていた、google が開発している JSON Schema ライブラリ
- JSON Schema バージョンは Draft 2020-12, Draft 7 をサポート
- invopop/jsonschema
- reference
- Inference(Goの型からのスキーマ生成)に強みを持つ
- JSON Schema バージョンは Draft 2020-12 をサポート
- santhosh-tekuri/jsonschema
- reference
- 2017年にv1がリリースされるなど歴史が長く、現在v6まで出ている
- JSON Schema バージョンは最新を含むほぼ全てをサポート
- ianlancetaylor/jsonschema
- reference
- Go の 元コアコミッターである、Ian Lance Taylor 氏が実装したライブラリ
- JSON Schema バージョンは Draft 2020-12, Draft 2019-09, Draft 7 をサポート
では、各ライブラリを見ていきましょう。
google/jsonschema-go
約70個のフィールドを持つ Schema 構造体を使用しています。JSON Schema のキーワードが1対1でフィールドにマッピングされており、Extra map[string]any で非標準のキーワードも格納できます。
type Schema struct { Type string Properties map[string]*Schema Items *Schema Required []string Minimum *float64 Maximum *float64 // ... 約70フィールド Extra map[string]any }
Draft 7 と Draft 2020-12 のバージョン差分は、単一の構造体の中にバージョン固有のフィールドを両方持つことで吸収しています。例えば、Draft 7 の definitions と Draft 2020-12 の $defs に対応する Definitions と Defs が共存しています。
type Schema struct { // draft 7 Definitions map[string]*Schema // "definitions" ItemsArray []*Schema // "items" (配列形式) DependencySchemas map[string]*Schema // "dependencies" (スキーマ) // draft 2020-12 Defs map[string]*Schema // "$defs" PrefixItems []*Schema // "prefixItems" DependentSchemas map[string]*Schema // "dependentSchemas" // ... }
Marshal/Unmarshal 時に $schema の値からドラフトを判定し、適切なフィールドへの振り分けやキーワードの出し分けを行います。
素朴かつ全てをサポートした巨大な構造体なので、構造体リテラルでの手動構築、json.Unmarshal によるパース、For[T]() による Go の型からの推論と、スキーマの構築方法を幅広くサポートしています。$ref の解決は明示的な Resolve() メソッドで行い、スキーマのバリデーションにも対応しています。
invopop/jsonschema
約60個のフィールドを持つ Schema 構造体を採用しています。Properties には orderedmap.OrderedMap[string, *Schema] を使用しており、順序も含めて管理することができます。
type Schema struct { Type string `json:"type,omitempty"` Properties *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"` Items *Schema `json:"items,omitempty"` Required []string `json:"required,omitempty"` // ... 約60フィールド Extras map[string]any `json:"-"` }
Draft 2020-12 のみをサポートしており、バージョン差分の吸収は行いません。フィールドは $defs、prefixItems など 2020-12 のキーワードに対応しています。
google/jsonschema-go と同じく素朴な構造体ですが、フィールドの型に違いがあります。数値制約(minimum など)には *float64 ではなく json.Number を、整数値キーワード(maxLength、minLength、maxItems など)には *int ではなく *uint64 を使用しており、JSON の元表現の保持や、負の数の型レベルでの排除を意識した設計です。
一方で、Draft 2020-12 のキーワードでも 現時点では unevaluatedItems、unevaluatedProperties、$dynamicAnchor、$vocabulary には対応していません。
こちらはスキーマ生成に特化したライブラリです。主要な構築方法は Reflector を使った Go の型からの推論で、構造体タグで title、minLength、enum などを細かく制御できます。一方、$ref の解決やバリデーション機能は提供していません。
santhosh-tekuri/jsonschema
Schema 構造体を使います。他のライブラリと異なる点が2つあります。
1つ目は、数値制約に *big.Rat を使っており、浮動小数点誤差を回避していることです。2つ目は、$ref が文字列ではなく、コンパイル時に解決された *Schema ポインタとして保持されることです。Compiler が JSON をパースしてスキーマを構築する際に、参照の解決も同時に行われます。
type Schema struct { Ref *Schema // 解決済みの直接ポインタ Types *Types Minimum *big.Rat Maximum *big.Rat Properties map[string]*Schema // ... }
バージョン差分は Draft 型で管理されます。各 Draft はキーワードの集合やサブスキーマの場所、$id のフィールド名(Draft 4 は id、Draft 6 以降は $id)などをまとめて管理しており、Draft 4 から Draft 2020-12 まで、ほぼすべてのバージョンをサポートしています。Compiler が $schema キーワードからドラフトを自動検出し、異なるドラフトのスキーマを混在させることも可能です。
var ( Draft4 *Draft Draft6 *Draft Draft7 *Draft Draft2019 *Draft // draft 2019-09 Draft2020 *Draft // draft 2020-12 )
スキーマの構築は Compiler 経由のみで、プログラマティックな組み立てや Go の型からの推論は提供されていません。バリデーションがこのライブラリの主機能で、JSON Schema 仕様に準拠した複数のエラー出力フォーマットに対応しています。
ianlancetaylor/jsonschema
他の3つとは全く異なるアプローチを取っています。固定フィールドの構造体でも map[string]any でもなく、キーワードと型付き値のペアのリストで表現します。
type Schema struct { Parts []Part } type Part struct { Keyword *Keyword Value PartValue // 12種類の型付きユニオン }
PartValue は PartString、PartInt、PartSchema、PartSchemas など12種類の型からなる型付きユニオンです。
この設計はバージョン差分の吸収と密接に結びついています。ドラフトごとに別の Go パッケージが用意され、各パッケージが独自の Vocabulary を登録します。Vocabulary はドラフトの名前、$schema の URI、キーワードの集合、参照解決関数などをまとめた型です。
type Vocabulary struct { Name string // "draft2020-12" など Schema string // $schema の URI Keywords map[string]*Keyword // キーワード名 → 定義 Resolve func(*Schema, *ResolveOpts) error Cmp func(string, string) int } type Keyword struct { Name string ArgType ArgType // PartString, PartSchema など期待する値の型 Validate func(arg PartValue, instance any, state *ValidationState) error }
各ドラフトパッケージは init() で自身の Vocabulary をグローバルレジストリに登録します。キーワードは JSON の定義ファイルからコード生成されるため、ドラフト間の条件分岐が一切ありません。スキーマの構造がバージョンに依存せず、異なるドラフトバージョンは単に異なるキーワードセットを登録するだけでよくなります。ただし、結局キーワードセットは巨大になるので、そう言う意味では他のとあまり変わらない、とも言えるかもしれません。
// ドラフトごとに別パッケージ import "github.com/ianlancetaylor/jsonschema/draft202012" import "github.com/ianlancetaylor/jsonschema/draft7"
内部表現がキーワードリストであるため、構築 API も独特です。構造体リテラルではなく、ドラフトバージョン別パッケージが提供する API を使って Builder パターンでスキーマを組み立てます。JSON のパースや Infer[T]() による Go の型からの推論にも対応しています。$ref の解決はアンマーシャル時に自動で行われ、バリデーションもサポートされています。
横断比較
| ライブラリ | 内部表現 | スキーマ構築 | バージョン差分 | 参照解決 | バリデーション |
|---|---|---|---|---|---|
| google/jsonschema-go | struct(両バージョンのフィールド共存) | struct リテラル / JSON / For[T]() |
単一structに両バージョン | 明示的 Resolve() |
あり |
| invopop/jsonschema | struct | struct リテラル / JSON / Reflector(主力) |
Draft 2020-12 のみ | 生成のみ | なし |
| santhosh-tekuri/jsonschema | struct(*big.Rat) |
Compiler 経由のみ |
Draft 型 + 自動検出 |
コンパイル時にポインタ解決 | あり(主機能) |
| ianlancetaylor/jsonschema | []Part |
Builder / JSON / Infer[T]() |
ドラフト別パッケージ | アンマーシャル時に自動 | あり |
まとめ
4つのライブラリを比較してみると、JSON Schema の表現方法にはいくつかのアプローチがあることがわかります。
- google/jsonschema-go と invopop/jsonschema は、JSON Schema のキーワードを構造体のフィールドに直接マッピングする素朴なアプローチです。google/jsonschema-go は両バージョンのフィールドを共存させてバージョン差分を吸収し、invopop/jsonschema は Draft 2020-12 に絞ることでシンプルさを保っています
- santhosh-tekuri/jsonschema も構造体ベースですが、コンパイル時の
$refのポインタ解決など、バリデーション用途に最適化された表現を選んでいます - ianlancetaylor/jsonschema は、キーワードとペアのリストという独自の表現で、バージョン差分をデータ構造レベルで吸収する大胆な設計です
同じ「Go で JSON Schema を扱う」という目的でも、内部表現の選択がバージョン対応や API 設計にまで影響を及ぼしていることが面白かったです。この記事を通して、Go で JSON Schemaを扱うことに興味を持っていただければ幸いです。