every Tech Blog

株式会社エブリーのTech Blogです。

Go の JSON Schema ライブラリたちはどのように JSON Schema を表現しているか

Go の JSON Schema ライブラリたちはどのように JSON Schema を表現しているか

目次

はじめに

こんにちは、開発本部開発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 のキーワードは typepropertiesitemsminimummaximumpatternallOfanyOf$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 に対応する DefinitionsDefs が共存しています。

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 のみをサポートしており、バージョン差分の吸収は行いません。フィールドは $defsprefixItems など 2020-12 のキーワードに対応しています。

google/jsonschema-go と同じく素朴な構造体ですが、フィールドの型に違いがあります。数値制約(minimum など)には *float64 ではなく json.Number を、整数値キーワード(maxLengthminLengthmaxItems など)には *int ではなく *uint64 を使用しており、JSON の元表現の保持や、負の数の型レベルでの排除を意識した設計です。

一方で、Draft 2020-12 のキーワードでも 現時点では unevaluatedItemsunevaluatedProperties$dynamicAnchor$vocabulary には対応していません。

こちらはスキーマ生成に特化したライブラリです。主要な構築方法は Reflector を使った Go の型からの推論で、構造体タグで titleminLengthenum などを細かく制御できます。一方、$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種類の型付きユニオン
}

PartValuePartStringPartIntPartSchemaPartSchemas など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-goinvopop/jsonschema は、JSON Schema のキーワードを構造体のフィールドに直接マッピングする素朴なアプローチです。google/jsonschema-go は両バージョンのフィールドを共存させてバージョン差分を吸収し、invopop/jsonschema は Draft 2020-12 に絞ることでシンプルさを保っています
  • santhosh-tekuri/jsonschema も構造体ベースですが、コンパイル時の $ref のポインタ解決など、バリデーション用途に最適化された表現を選んでいます
  • ianlancetaylor/jsonschema は、キーワードとペアのリストという独自の表現で、バージョン差分をデータ構造レベルで吸収する大胆な設計です

同じ「Go で JSON Schema を扱う」という目的でも、内部表現の選択がバージョン対応や API 設計にまで影響を及ぼしていることが面白かったです。この記事を通して、Go で JSON Schemaを扱うことに興味を持っていただければ幸いです。