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.
- Web UI: https://presentation-validator.iiif.io/
- Source: https://github.com/IIIF/presentation-validator
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.
- GitHub: https://github.com/iiif-prezi/iiif-prezi3
- PyPI:
iiif-prezi3
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
| Aspect | Official presentation-validator | iiif-prezi3 |
|---|---|---|
| Primary purpose | Dedicated validation | Library for creating and manipulating manifests (validation is a side effect of pydantic) |
| Validation logic | JSON Schema (schema/iiif_3_0.json) plus custom Python checks | pydantic v2 model: type, required fields, URL format |
| Distribution | Web service at presentation-validator.iiif.io, or self-hosted Flask app | Python package (pip install iiif-prezi3) |
| Error counting | Roughly one error per schema violation | One per union branch — a single cause tends to expand to about 10–12 entries |
| Error messages | Close 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) |
| Detail | When a oneOf branch fails, detail may be empty and the specific branch/reason can be hard to read | Each branch and its failure are listed exhaustively |
| Coverage | Custom checks cover spec details beyond what JSON Schema can express | What the pydantic model covers (main structural rules) |
| Programmatic use | Fetch JSON over HTTP and parse | Work 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 locally —
iiif-prezi3is 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
- IIIF Presentation API 3.0 spec — https://iiif.io/api/presentation/3.0/
- IIIF Presentation Validator — https://presentation-validator.iiif.io/ / https://github.com/IIIF/presentation-validator
iiif-prezi3— https://github.com/iiif-prezi/iiif-prezi3




Comments
…