Skip to content

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 the fair2-spec release tag, and resolving to that version's documentation. It is required, and fair2s:DatasetShape enforces it with a pattern.
  • version is the dataset's own release.
  • dateCreated / datePublished / dateUpdated are 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.

  1. Land the spec changes on main.
  2. Generate the frozen context: python tools/build_context.py vX.Y.Z.
  3. Bump conformsTo in the example(s) to https://fair2.ai/spec/vX.Y.Z.
  4. 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.
  5. Commit, then tag: git tag -a vX.Y.Z -m "…" && git push origin vX.Y.Z. The docs workflow publishes vX.Y.Z, moves the latest alias and sets it as the site default.
  6. In consumers, advance the fair2-spec reference 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.