Versioning & spec conformance¶
Declaring the spec version — dct:conformsTo¶
Every FAIR² document declares which version of the specification it was
produced against, as one of the Dataset's conformsTo values:
{
"@context": { "...": "..." },
"@id": "https://doi.org/10.1234/dataset",
"@type": "Dataset",
"conformsTo": [
"http://mlcommons.org/croissant/1.0",
"https://fair2.ai/spec/v1.4.0"
],
"version": "1.2",
"dateCreated": "2025-02-05",
"datePublished": "2025-03-03",
"dateUpdated": "2025-11-19"
}
dct:conformsTo is multi-valued by design, so one Dataset can declare both
the Croissant version and the FAIR² spec version without a second mechanism.
- the FAIR² spec version is a URL of the form
https://fair2.ai/spec/v<MAJOR>.<MINOR>.<PATCH>, matching thefair2-specrelease tag, and resolving to that version's documentation. It is required, andfair2s:DatasetShapeenforces it with a pattern. versionis the dataset's own release.dateCreated/datePublished/dateUpdatedare its lifecycle dates.
Two versions, deliberately¶
The dataset and the document describing it change on different schedules, so they carry separate version lines:
| Property | Versions | Bumped when |
|---|---|---|
version |
the dataset | the data itself changes |
fair2:metadataVersion |
the fair2.json document |
the description changes |
dateUpdated |
the dataset | the data last changed |
fair2:metadataModified |
the document | the file was last edited |
A dataset at 1.2 may have a metadata document at 1.3.2 after several
corrections. metadataVersion follows MAJOR.MINOR.PATCH:
| Bump | For |
|---|---|
| Patch | a metadata-only correction — typo, URL fix, date fix |
| Minor | an additive change — a new contributor, distribution or field |
| Major | a structural change — entity model redesign, context change |
Both are required, and fair2s:DatasetShape enforces the version pattern.
Fixing a typo in a description must not bump version, because the data did
not change.
Changed in v1.4.0 — the _meta block is gone
Up to v1.3.0 these lived in a _meta object outside @graph,
deliberately kept out of the RDF. That had three costs. The fields
duplicated Dataset properties and could disagree with them silently — in
the reference example _meta claimed version 1.0.0 and a modification
date of 2026-04-20 while the Dataset and its own changelog said 1.2
and 2025-11-19. Spec conformance could not be checked by SHACL, so it
needed deterministic enforcement in a separate validator. And because
_meta produced no triples, it was invisible to any digest taken over
the graph.
The spec version became a conformsTo value and the file's own version
and edit date became fair2:metadataVersion and
fair2:metadataModified, so the file-versus-dataset distinction _meta
drew is preserved — now in RDF, visible to a digest and checkable by
SHACL. _meta.dateCreated is the one field not carried over; in practice
it recorded the first release date, which datePublished already holds.
Consumers reading pre-v1.4.0 documents will still find _meta at the
root.
The versioned context¶
Each release freezes the JSON-LD context as context/vX.Y.Z.jsonld and
publishes it at https://fair2.ai/context/vX.Y.Z.jsonld. A published
context is immutable: documents point at it, so changing one would change
the meaning of every document already referring to it.
{
"@context": "https://fair2.ai/context/v1.5.0.jsonld",
"@id": "https://doi.org/10.1234/dataset",
"@type": "Dataset"
}
| File | Role |
|---|---|
context/metadata/fair2_context.json |
the working source, edited by hand |
context/vX.Y.Z.jsonld |
the frozen release, generated and never edited |
tools/build_context.py vX.Y.Z renders one from the other and refuses to
overwrite an existing version; the consistency guard fails if a published
context and the working source disagree. The relationship is the same one
shapes/json-ld/ has to shapes/turtle/: generated, checked, not
hand-maintained.
Every release cuts a context, even an unchanged one. v1.5.2's context
is term-for-term identical to v1.5.0's; it exists so that one rule holds
without exception:
conformsTo= the release tag = the frozen context version
The alternative — cutting a context only when the terms change — saves a duplicated file and costs a lookup table for which context a given release uses, plus an exception to remember. The duplication is about 14 KB.
The published form is flattened. The working source is a context array —
schema.org, Croissant, then the inline terms — while the published file
carries only the term map. That resolves in a single request, works offline
behind a document loader, and cannot
be altered by an upstream change to either vocabulary. Nothing is lost: the
reference example has used the flattened form throughout, and both
mlcroissant and pyshacl read it to the same graph.
Not yet referenced by URL
Documents still carry the context inline. Switching @context to the URL
waits until the file is actually served at fair2.ai, because until then
every consumer would fail to resolve it. The frozen file is generated and
guarded from v1.5.0 so that the switch is a one-line change per document
when the hosting is in place.
Versioned documentation¶
The specification site is versioned with
mike. Each fair2-spec release tag vX.Y.Z
publishes a corresponding docs version:
| URL | Points to |
|---|---|
https://fair2.ai/spec/v1.2.0/ |
the docs for release v1.2.0 |
https://fair2.ai/spec/latest/ |
the newest release (moving alias) |
https://fair2.ai/spec/ |
redirects to latest |
Only tagged releases are published — there is no intermediate/dev docs
channel. Each _meta.conformsTo therefore always resolves to a real release.
So a document's _meta.conformsTo URL is a stable, resolvable link to the exact
spec version it was built against, and …/spec/latest/ always redirects readers
to the current release.
Cutting a new spec version¶
The order matters: everything that names the version must be in the commit the tag points at.
- Land the spec changes on
main. - Generate the frozen context:
python tools/build_context.py vX.Y.Z. - Bump
conformsToin the example(s) tohttps://fair2.ai/spec/vX.Y.Z. - Run
python tools/check_spec_consistency.py. It fails if an example declares a different version from the newest frozen context, so steps 2 and 3 cannot be half-done. - Commit, then tag:
git tag -a vX.Y.Z -m "…" && git push origin vX.Y.Z. The docs workflow publishesvX.Y.Z, moves thelatestalias and sets it as the site default. - In consumers, advance the
fair2-specreference to the new tag.
Two releases shipped with a stale declaration
conformsTo used to be bumped at step 3, after tagging, so following
the list faithfully produced a tag whose example named the previous
version. v1.5.0 shipped declaring v1.4.0.
The first fix was itself incomplete: it compared the example against the newest frozen context, which is sound only if one exists per release. v1.5.1 was tagged without cutting a context, so the anchor stayed at v1.5.0 and v1.5.1 shipped declaring v1.5.0. Both halves are checked from v1.5.2 — the context must not lag the newest tag, and the example must match the newest context.