本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

IIIF (International Image Interoperability Framework) Presentation API 3.0 のマニフェストを受け取って、仕様への適合性を確認する機会がありました。検証手段として広く知られているのは公式の IIIF/presentation-validator ですが、Python ライブラリの iiif-prezi3 でもマニフェストの構造検証ができます。本記事では両者を実際のマニフェストに対して併用し、出力と用途の違いをまとめました。

公式 validator については過去にも紹介記事があるようですが、iiif-prezi3 を検証ツールとして使う観点での比較情報は調査した限り多くないため、こちらに比重を置いています。

検証する対象

説明用に、IIIF Presentation 3.0 形式の小さなマニフェスト例を用意します。1 canvas で 1 画像を持つ最小構成で、body.id だけ意図的に相対 URL (?IIIF=... で始まるもの) にしてあります。

{
  "@context": "http://iiif.io/api/presentation/3/context.json",
  "id": "https://example.org/iiif/book1/manifest",
  "type": "Manifest",
  "label": {"ja": ["サンプル資料"]},
  "items": [
    {
      "id": "https://example.org/iiif/book1/canvas/p1",
      "type": "Canvas",
      "label": {"ja": ["第1丁"]},
      "height": 3000,
      "width": 2000,
      "items": [{
        "id": "https://example.org/iiif/book1/canvas/p1/page",
        "type": "AnnotationPage",
        "items": [{
          "id": "https://example.org/iiif/book1/canvas/p1/page/anno",
          "type": "Annotation",
          "motivation": "painting",
          "target": "https://example.org/iiif/book1/canvas/p1",
          "body": {
            "id": "?IIIF=book1/p1.tif/full/max/0/default.jpg",
            "type": "Image",
            "format": "image/jpeg",
            "height": 3000,
            "width": 2000,
            "service": [{
              "id": "https://images.example.org/iiif/book1/p1.tif",
              "type": "ImageService3",
              "profile": "level2"
            }]
          }
        }]
      }]
    }
  ]
}

IIIF Presentation API 3.0 §3.2 (Technical Properties) では、id プロパティの値は HTTP(S) の絶対 URI でなければならないと規定されています。このマニフェストはその点で仕様に違反した状態です。比較のため、body.id を絶対 URL (https://images.example.org/iiif/book1/p1.tif/full/max/0/default.jpg) に置き換えた修正版も用意しておきます。

公式 IIIF Presentation Validator

公式の検証サービスは Web UI と HTTP API の両方で提供されています。

API は URL を指定する GET と、JSON ボディを POST する 2 通りで使えます。Validator のソースは Python 実装で、schema/iiif_3_0.json の JSON Schema に加えてカスタムチェックを行っています。

公開されているマニフェストの URL を渡す場合:

curl "https://presentation-validator.iiif.io/validate?url=<MANIFEST_URL>&version=3.0&format=json"

ローカルのファイルを直接検証する場合は POST を使います。

curl -X POST \
  "https://presentation-validator.iiif.io/validate?version=3.0&format=json" \
  -H "Content-Type: application/json" \
  --data-binary @broken.json

先ほどの不適合マニフェストに対する応答(要点のみ抜粋):

{
  "okay": 0,
  "errorList": [
    {
      "path": "/items[0]/items[0]/items[0]/body/[...]",
      "detail": ""
    }
  ]
}

okay: 0 で不合格、エラー件数は 1 件です。今回のケースでは oneOf スキーマの分岐失敗としてまとめて報告されるため、detail が空になっています。body.id を絶対 URL に直した修正版を投げると okay: 1errorList: [] で合格になります。

iiif-prezi3

iiif-prezi3 は IIIF コミュニティ公式の Python ライブラリで、Presentation API 3.0 リソースを生成・操作するためのものです。pydantic v2 のモデルとして v3 のデータモデルを実装しているため、JSON をモデルに流し込むと pydantic が自動で構造検証を行います。

インストール:

pip install iiif-prezi3

最小の検証コード:

from iiif_prezi3 import Manifest

raw = open("broken.json").read()
try:
    Manifest.model_validate_json(raw)
    print("PASS")
except Exception as e:
    print(f"FAIL: {len(e.errors())} sub-errors")
    for er in e.errors()[:3]:
        loc = ".".join(str(x) for x in er["loc"])
        print(f"  {loc}: {er['msg']}")

先ほどの不適合マニフェストに対する出力:

FAIL: 12 sub-errors
  items.0.items.0.items.0.body.Resource.AnnotationBody.id:
    Input should be a valid URL, relative URL without a base
  items.0.items.0.items.0.body.Resource.TextualBody.id:
    Input should be a valid URL, relative URL without a base
  items.0.items.0.items.0.body.Resource.TextualBody.type:
    String should match pattern '^TextualBody$'

エラー件数が 12 件として表示されていますが、本質的な原因は body.id が相対 URL になっていることの 1 点に集約されます。これは pydantic がユニオン型(AnnotationBody | TextualBody | SpecificResource | ...)の各分岐ごとに失敗を独立に報告するためで、1 つの原因が複数の派生エラーとして展開される性質によるものです。修正版を投げると PASS が返ります。

両ツールの挙動の違い

実利用で気になったポイントを表にまとめます。

観点公式 presentation-validatoriiif-prezi3
主目的バリデーション専用manifest 作成・操作ライブラリ(バリデーションは pydantic 由来の副次機能)
検証ロジックJSON Schema (schema/iiif_3_0.json) + カスタム Python チェックpydantic v2 モデルによる型・必須・URL format 検証
配布Web サービス (presentation-validator.iiif.io)、Flask 自前ホスト可Python パッケージ (pip install iiif-prezi3)
エラー件数の出方スキーマ違反 1 箇所につき概ね 1 件ユニオン分岐ごとに 1 件ずつ展開(1 原因あたり 10〜12 件程度に膨らむ傾向)
エラーメッセージ仕様文言に近い表現 (id Id must be present and must be a URI 等)pydantic 由来 (Input should be a valid URL, relative URL without a base 等)
詳細さoneOf 分岐失敗時に detail が空になり、どの分岐でなぜ落ちたかが読み取りにくい場合があるどの分岐でどう失敗したかが網羅的に出る
カバレッジカスタムチェックで仕様の細部までフォローpydantic モデルが網羅する範囲(主要な構造はカバー)
プログラムからの扱いHTTP 経由で JSON を取得して処理Python オブジェクトとして直接扱える

エラー件数のインフレーションについて

50 件程度のマニフェストを両ツールに通したところ、公式 validator のエラー総数に対して iiif-prezi3e.errors() 総数は約 10.5 倍でした。1 件のマニフェスト内で見ても 10〜12 倍前後で、概ね定数倍の関係になっていました。

「原因の重複報告で件数が膨らむ」性質は、初見だと「prezi3 のほうが検査が厳しい」と誤読しやすいので、注意点として記録しておきます。実際には両ツールが検出している不適合の原因は同じでした。

独立した 2 実装が同じ結論を出すことの意味

検証ツール選びの観点では、ひとつ気付きがありました。両ツールは

  • IIIF Presentation API 3.0 という同じ仕様を参照しているものの
  • 実装言語・検証アプローチ (JSON Schema vs pydantic モデル) はまったく別
  • 開発者・コードベースも独立

という関係にあります。実検証で「両ツールが同じ箇所を不適合と判定した」という結果は、

単に一方の validator が厳しすぎるのではなく、IIIF v3 仕様そのものに対する違反である

という解釈の補強材料になります。1 つの validator だけで判定すると「実装の癖では?」という反論余地が残りますが、独立 2 実装で結論が一致していれば、その余地は小さくなります。

報告書類で v3 違反を指摘する場面では、両ツールに通した結果を併記しておくと根拠の強度が増す、というのが今回の実用上の知見でした。

使い分け

実際の使い方として整理すると次のようになりそうです。

  • ローカルで Python から繰り返し叩きたいiiif-prezi3 が手軽。pytest と組み合わせて CI に乗せやすい
  • 権威ある検証結果として外部に出したい — 公式 validator の出力を引用する方が筋が通りやすい
  • 両方使って結論を一致させる — 報告書・記事等で v3 違反を指摘する場合の標準パターンとして有効

iiif-prezi3 は本来「マニフェストを作る側」のライブラリですが、Manifest.model_validate_json() を呼ぶだけで構造検証として転用できるため、外部から受け取ったマニフェストの受け入れチェックや自動テストにも組み込みやすい点が便利でした。

参考