This article is co-written with generative AI. Facts have been cross-checked against official documentation where possible, but errors may remain. Please confirm primary sources before making important decisions.

I recently had occasion to receive IIIF (International Image Interoperability Framework) Presentation API 3.0 manifests and to check their conformance to the specification. The official IIIF/presentation-validator is well known for this task, but the Python library iiif-prezi3 can also perform structural validation of manifests. This post records what I learned from running both against actual manifests, focusing on how their output differs and when to use which.

There appear to be existing posts about the official validator, but in my own search I found relatively little written about using iiif-prezi3 as a validation tool, so the post leans toward that side.

The manifest to validate

For illustration, here is a minimal IIIF Presentation 3.0 manifest with one canvas and one image. The body.id is intentionally set to a relative URL (one starting with ?IIIF=...).

{
  "@context": "http://iiif.io/api/presentation/3/context.json",
  "id": "https://example.org/iiif/book1/manifest",
  "type": "Manifest",
  "label": {"en": ["Sample"]},
  "items": [
    {
      "id": "https://example.org/iiif/book1/canvas/p1",
      "type": "Canvas",
      "label": {"en": ["Folio 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) states that the value of id must be an HTTP(S) absolute URI, so this manifest does not conform on that point. For comparison, I will also use a fixed version in which body.id is replaced with an absolute URL (https://images.example.org/iiif/book1/p1.tif/full/max/0/default.jpg).

The official IIIF Presentation Validator

The official validation service exposes both a Web UI and an HTTP API.

The API supports both a GET form that takes a URL and a POST form that takes the manifest body. The implementation is Python and uses a JSON Schema (schema/iiif_3_0.json) along with additional custom checks.

To validate a hosted manifest URL:

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

To validate a local file, use POST:

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

The response for the non-conforming manifest above (relevant parts only):

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

okay: 0 means failed; the error list has one entry. In this case the failure is reported as a oneOf schema branch mismatch, and detail ends up empty. Passing the fixed version (with body.id as an absolute URL) instead returns okay: 1 and an empty errorList.

iiif-prezi3

iiif-prezi3 is the IIIF community's official Python library for working with Presentation API 3.0 resources. It implements the v3 data model as pydantic v2 models, so loading JSON into a model automatically performs structural validation as a side effect.

Install:

pip install iiif-prezi3

A minimal validation snippet:

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']}")

Output for the non-conforming manifest above:

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$'

The library reports 12 sub-errors, but the underlying cause comes down to a single point: body.id is a relative URL. The fan-out happens because pydantic reports failures separately for each branch of a union type (AnnotationBody | TextualBody | SpecificResource | ...), so one root cause expands into several derived errors. Passing the fixed version returns PASS.

How the two tools differ

AspectOfficial presentation-validatoriiif-prezi3
Primary purposeDedicated validationLibrary for creating and manipulating manifests (validation is a side effect of pydantic)
Validation logicJSON Schema (schema/iiif_3_0.json) plus custom Python checkspydantic v2 model: type, required fields, URL format
DistributionWeb service at presentation-validator.iiif.io, or self-hosted Flask appPython package (pip install iiif-prezi3)
Error countingRoughly one error per schema violationOne per union branch — a single cause tends to expand to about 10–12 entries
Error messagesClose to spec wording (e.g. id Id must be present and must be a URI)pydantic-style (e.g. Input should be a valid URL, relative URL without a base)
DetailWhen a oneOf branch fails, detail may be empty and the specific branch/reason can be hard to readEach branch and its failure are listed exhaustively
CoverageCustom checks cover spec details beyond what JSON Schema can expressWhat the pydantic model covers (main structural rules)
Programmatic useFetch JSON over HTTP and parseWork directly with Python objects

About the error count inflation

In a run over about 50 manifests, the total error count from the official validator and the total e.errors() from iiif-prezi3 differed by roughly a factor of 10.5×. Per-manifest the ratio stayed in the 10–12× range — essentially a constant multiplier.

The "one cause reported many times" behaviour can be misread as "iiif-prezi3 is stricter" if you only glance at the totals. In practice, both tools were flagging the same underlying violations in our experiments.

Why having two independent implementations agree matters

There is one practical observation worth recording. The two tools are

  • referring to the same specification (IIIF Presentation API 3.0)
  • using different languages and validation strategies (JSON Schema vs. pydantic models)
  • maintained by different teams on different code bases

If both flag the same location as non-conforming, it supports the reading that

the manifest violates the IIIF v3 specification itself, rather than tripping over an idiosyncrasy of one validator.

With only one validator's output, there is room to argue that "this is just that tool's quirk". With two independent implementations agreeing, that room narrows considerably.

When pointing out v3 violations in a written report, citing both tools' results side by side has been useful for raising the strength of the evidence — that has been the main practical takeaway for me.

When to use which

A rough guide to picking between them:

  • Run validation repeatedly from Python locallyiiif-prezi3 is convenient and easy to drop into pytest / CI
  • Use the result as an authoritative external citation — quoting the official validator's output is usually cleaner
  • Run both and cross-check — a sensible default when writing up v3 violations in a report or article

iiif-prezi3 is primarily a library for producing manifests, but a single call to Manifest.model_validate_json() repurposes it for structural validation, which makes it straightforward to bolt on as an intake check or automated test for incoming manifests.

References