📖 New to this convention? Start with the introduction to the
coordsconvention for the motivation and a guided walkthrough.
- UUID: 6ca4454a-658a-4348-a667-b39ced0e58cb
- Name: "coords"
- Namespace:
coords: - Schema URL: "https://raw.githubusercontent.com/zarr-conventions/coords/refs/tags/v1/schema.json"
- Spec URL: "https://github.com/zarr-conventions/coords/blob/v1/README.md"
- Extension Maturity Classification: Proposal
- Owner: @christophenoel
Domain-agnostic mapping between Zarr array index space and coordinate space.
This convention associates each dimension of a Zarr array (or tuple of dimensions, for multi-dimensional coordinates) with a coordinate descriptor that says how the coordinate values are represented and where they live. It treats every coordinate kind uniformly — temporal, vertical, spectral, categorical, spatial, or domain-specific — and applies equally to regular, irregular, and curvilinear axes.
It deliberately does not invent a new coordinate model. Instead it offers a small set of descriptor shapes:
- Explicit coordinate arrays — a sibling Zarr array holds the per-index
values for the dimension (the NetCDF / Xarray model). 1-D for regular
axes such as
time(time),level(level), orband(band); N-D where coordinates depend on more than one index dimension. - Inline coordinate values — short value vectors embedded directly in the metadata (for example, a 4-band spectral axis where allocating a separate array would be wasteful).
- Implicit regularly spaced values — a compact
start/end/stepdescriptor for axes that are uniformly spaced, covering both numeric domains (angles, distances, frequencies, levels) and ISO 8601 time intervals, without enumerating every value. - Delegation via composition — for coordinate kinds that have their
own Zarr convention, the descriptor delegates to it via a generic
referencedescriptor instead of carrying the values. Future conventions for temporal calendars, vertical levels, spectral bands, lookup tables, or other domains plug in this way. - Geospatial use — spatial coordinates follow one of two traditions:
explicit
lat/lon(or projectedx/y) arrays — the NetCDF / CF model, sometimes paired withgrid_mapping— use thearraydescriptor above; an affine geotransform on a regular grid — the GeoTIFF / GDAL model — uses areferencedescriptor delegating to thespatialconvention.
All properties use the coords: namespace prefix and are placed at the root
attributes level following the Zarr Conventions Specification.
- Provides a uniform way to declare what each axis of a Zarr array means and where its coordinate values come from, regardless of whether the axis is spatial, temporal, spectral, or domain-specific.
- In the geospatial domain, bridges two ecosystems for spatial coordinates:
- NetCDF / CF / Xarray — explicit
lat/lon(or projectedx/y) arrays, often paired with the CFgrid_mappingattribute. CF semantic metadata (standard_name,units,axis,grid_mapping) is out of scope here and may be formalized by a futurecf:convention. - GeoTIFF / GDAL / GeoZarr — affine geotransform expressed by the
spatialconvention.
- NetCDF / CF / Xarray — explicit
- Composable with
spatial(affine georeferencing),proj(CRS), andmultiscales(each level can independently declare its own coordinates). - Uses integer-major + URL pin versioning: the
schema_urlcarries the major (/refs/tags/v1/schema.json); all v1.x changes are additive.
spatial— when a spatial axis is represented by an affine geotransform (the GeoTIFF / GDAL model), declare it with{type: "reference", convention: "spatial"}and let the existingspatial:transformattribute carry the matrix. Spatial axes represented as explicitlat/lon(or projected-x/ projected-y) arrays — the NetCDF / CF model, sometimes paired withgrid_mapping— use thearraydescriptor instead.proj— provides the CRS for spatial axes; orthogonal tocoords:, applied on the same node.multiscales— domain-agnostic per-axis resampling pyramid. Each level independently carries its owndimension_namesandcoords:coordinates, so pyramids can downsample non-spatial axes (time, band, …) just as uniformly as spatial ones. See Relationship with themultiscalesconvention.
The convention must be registered in zarr_conventions:
{
"zarr_conventions": [
{
"schema_url": "https://raw.githubusercontent.com/zarr-conventions/coords/refs/tags/v1/schema.json",
"spec_url": "https://github.com/zarr-conventions/coords/blob/v1/README.md",
"uuid": "6ca4454a-658a-4348-a667-b39ced0e58cb",
"name": "coords",
"description": "Domain-agnostic mapping between Zarr array index space and coordinate space."
}
]
}This convention can be used with these parts of the Zarr hierarchy:
- Group
- Array
On arrays, coords:coordinates keys reference the array's own
Zarr v3 dimension_names (or, for auxiliary coordinates, name a coordinate
that indexed_by ties to those dimensions). On groups,
coords:coordinates can act as a group-level catalogue of coordinate
descriptors shared by child arrays; keys reference dimension names used by
those children.
This convention deliberately does not introduce a new field for
dimension names. Every Zarr v3 array carries a top-level
dimension_names field alongside zarr_format, node_type, shape,
etc.:
{
"zarr_format": 3,
"node_type": "array",
"dimension_names": ["time", "y", "x"],
"attributes": { /* coords:coordinates lives here */ }
}By default, each coords:coordinates map key MUST resolve to an entry in
that array's dimension_names — it names the dimension whose coordinate
values the descriptor supplies (a 1-D coordinate variable, in CF terms).
The one exception is an auxiliary coordinate declared with
indexed_by.
For Zarr v2 datasets, see Zarr v2 compatibility.
All properties use the coords: namespace prefix and are placed at the root
attributes level.
| Field Name | Type | Required | Description |
|---|---|---|---|
coords:coordinates |
object |
Required | Map from a coordinate key to a coordinate descriptor. Keys are dimension names from Zarr v3 dimension_names — except for auxiliary coordinates declared with indexed_by, where the key is a coordinate name you choose. See Dimension names. |
coords:version |
integer |
Optional | Major version pin (currently 1). Optional because schema_url already pins the major. |
Additional properties are allowed.
Map from a coordinate key to a coordinate descriptor.
- Type: object (map)
- Required: yes — a node that registers this convention MUST carry a
coords:coordinatesmap. - Keys: a dimension name from the array's Zarr v3
dimension_names. The sole exception is an auxiliary coordinate keyed by its own name — seeindexed_by. - Values: a Coordinate descriptor.
The keys alone — independent of the descriptor type, omitted here for
clarity — look like this for an array with
dimension_names: ["time", "y", "x"]:
Optional integer pinning the major version of the convention this metadata
was authored against. Currently 1. Readers MAY use it as a sanity check
in addition to the schema_url. Omitting it is fine — schema_url already
pins the major.
Each value in coords:coordinates is one of the following shapes,
distinguished by the type field. The fields defined below are the only
ones this convention specifies; descriptors carry additionalProperties: true, so other ecosystems can attach their own metadata without
conflicting — see Coordinate semantics are out of
scope.
{
"type": "array",
"path": "../time"
}pathis a Zarr-relative path to a sibling array holding the coordinate values.- The map key is the dimension name this coordinate belongs to, and the target array is 1-D along that dimension (a CF coordinate variable).
- For coordinates that vary along a different dimension, or along more than
one dimension, add
indexed_byand key the entry by the coordinate's own name.
indexed_by declares the dimension or dimensions on which a coordinate array
depends. Use it when a coordinate is associated with a dimension rather than
representing that dimension itself — so the map key is the coordinate's name
(e.g. lat), not a dimension name.
This is the CF auxiliary coordinate model. It covers point observations,
trajectories, profiles, stations, and curvilinear grids — where coordinates
like lat, lon, or time vary along some other dimension (sample,
station, …) or along several dimensions at once (y, x).
{
"type": "array",
"indexed_by": ["sample"],
"path": "../lat"
}Keyed as lat in coords:coordinates, this describes a latitude coordinate
array whose values are indexed by the sample dimension:
lat(sample)
Two or more dimensions give a multi-dimensional / curvilinear coordinate —
e.g. indexed_by: ["y", "x"] for lat(y, x). Each name in indexed_by MUST
be one of the array's dimension_names, and the referenced coordinate array's
own dimension_names MUST match indexed_by in order. Because the entry is
keyed by the coordinate name, several auxiliary coordinates can share the same
dimensions (lat(y, x) and lon(y, x)) without colliding.
In short: an explicit coordinate array indexed by one or more dimensions — for auxiliary coordinates such as
lat(sample),lon(sample),time(sample), orlat(y, x).
{
"type": "reference",
"convention": "spatial"
}Delegates this coordinate to a named sibling convention, which supplies or defines its values — this descriptor carries none.
The canonical case is convention: "spatial", shown above: the axis is
derived from the spatial:transform affine matrix declared on the same node
(or an ancestor group) — the GeoTIFF / GDAL representation of spatial
coordinates. Use it when georeferencing is already expressed via the
spatial convention and you
only need to map a dimension name to it. For the NetCDF / CF tradition
(explicit lat / lon or projected x / y arrays, often with a CF
grid_mapping attribute), use the array descriptor instead.
The same mechanism plugs in future coordinate families — temporal calendars,
vertical levels, spectral bands, lookup tables, or domain-specific axes
({type: "reference", convention: "<name>"}) — without expanding this
convention.
{
"type": "inline",
"values": [0.490, 0.560, 0.665, 0.842]
}Embeds the coordinate values directly in the metadata. Intended for short auxiliary axes (e.g. a 4-band spectral axis) where allocating a separate Zarr array would be wasteful. Keep these small — readers MAY refuse to interpret very large inline arrays.
{
"type": "interval",
"start": 0,
"end": 10,
"step": 2
}→ 0, 2, 4, 6, 8, 10
A compact descriptor for axes whose coordinate values form a regularly
spaced sequence. Equivalent to an inline descriptor enumerating
start, start + step, ..., end, but avoids materializing the values.
startis the first value of the sequence.endis inclusive: the last value of the sequence reachesendexactly when(end − start)is an integer multiple ofstep. Authors SHOULD ensure this; readers MAY ignore any residual fraction.stepis the increment between successive values. It MUST be non-zero; it MAY be negative for descending sequences.
Two value domains are supported, distinguished by JSON type:
Numeric — start, end, step are all JSON numbers. Applicable to
any ordered numeric axis: integers, floating-point values, angles,
distances, frequencies, elevations, levels:
{ "type": "interval", "start": 1.0, "end": 2.0, "step": 0.25 }→ 1.0, 1.25, 1.5, 1.75, 2.0
{ "type": "interval", "start": 0, "end": 360, "step": 15 }→ 0°, 15°, 30°, …, 360°
ISO 8601 (temporal) — start and end are ISO 8601 date-time
strings; step is an ISO 8601 duration (e.g. P1D, PT1H, P1M).
This is the temporal equivalent of the numeric form, standardized by
ISO 8601:
{
"type": "interval",
"start": "2026-01-01T00:00:00Z",
"end": "2026-01-31T00:00:00Z",
"step": "P1D"
}Conceptually equivalent to the ISO 8601 interval notation
2026-01-01T00:00:00Z/2026-01-31T00:00:00Z/P1D, but kept as three
discrete fields so readers do not need to parse a compound string.
Unit and calendar semantics (e.g. CF units, calendar for the numeric
form; calendar choice for the ISO 8601 form) remain out of scope — see
Coordinate semantics are out of scope.
A complete array node: the Zarr v3 dimension_names declare the axes, and
coords:coordinates routes each one to a descriptor — here an explicit
time array alongside y / x axes delegated to the spatial convention.
{
"zarr_format": 3,
"node_type": "array",
"dimension_names": ["time", "y", "x"],
"attributes": {
"zarr_conventions": [
{ "name": "coords", "schema_url": "https://raw.githubusercontent.com/zarr-conventions/coords/refs/tags/v1/schema.json" }
],
"coords:coordinates": {
"time": { "type": "array", "path": "../time" },
"y": { "type": "reference", "convention": "spatial" },
"x": { "type": "reference", "convention": "spatial" }
},
"coords:version": 1
}
}This is examples/coords.json (shown there with the
full zarr_conventions registration block). For the implicit regularly
spaced form, see examples/coords-interval.json.
This convention is deliberately limited to locating coordinates. It does not define how to describe their semantics — units of measure, calendar, axis role, standard name, etc. Those concerns belong to other specifications:
- The CF conventions define
standard_name,long_name,units,axis,calendar,grid_mapping, and related metadata. - A future dedicated
cf:Zarr convention may formalize a CF-aligned attribute namespace for use alongsidecoords:. Until such a convention exists, CF-style fields MAY appear directly inside a coordinate descriptor — descriptors areadditionalProperties: true, so readers that understand CF can pick them up whilecoords:validators ignore them.
This is the only example in this spec that includes CF fields. It is shown
to illustrate the future composition path; the underlined fields below are
not defined by coords: and are passed through verbatim:
{
"type": "array",
"path": "../time",
"standard_name": "time",
"long_name": "observation time",
"units": "seconds since 2020-01-01",
"axis": "T",
"calendar": "proleptic_gregorian"
}A future cf: convention would register the CF vocabulary explicitly
(via its own schema_url in zarr_conventions) and validate these
fields. For now, treat them as opportunistic interop metadata rather than
part of coords:.
Spatial coordinates have two well-established representations in the wider ecosystem:
- GeoTIFF / GDAL — an affine geotransform on a regular grid (origin +
cell size + CRS hook). This is what the
spatialconvention captures, and what areferencedescriptor withconvention: "spatial"delegates to. - NetCDF / CF / Xarray — explicit
lat/lon(or projectedx/y) coordinate arrays, often paired with a CFgrid_mappingattribute. In this spec these are just regular explicit coordinate arrays, declared via thearraydescriptor.
This convention is intentionally broader than spatial:
spatialdescribes the GeoTIFF / GDAL affine model for the spatial dimensions of a grid.coords:describes any dimension — temporal, vertical, spectral, categorical, spatial, or domain-specific — and how to find its coordinate representation, including both spatial representations above.
The two compose cleanly:
- Routing an existing affine grid. A dataset already using
spatial:transformwithdimension_names: ["y", "x"]can adoptcoords:to reuse the affine transform viacoords:coordinates: {"y": {"type": "reference", "convention": "spatial"}, "x": {"type": "reference", "convention": "spatial"}}. - Mixed spatial + non-spatial dimensions. A
(time, y, x)cube can carry atimeaxis described as an explicit coordinate array andy/xaxes delegated to thespatialconvention's affine transform — all declared uniformly inside onecoords:coordinatesmap. - Irregular / curvilinear / swath data. When the affine model does
not apply, declare the spatial coordinates as auxiliary
type: "array"descriptors keyed by name (lat,lon) withindexed_by: ["y", "x"]pointing at 2-Dlat(y, x)/lon(y, x)arrays. Thespatialconvention is simply not used in that case. - Future coordinate families. Temporal, vertical, spectral, lookup,
or domain-specific coordinate types can plug in either by extending the
typeenum in a future major version of this convention, or by adding their own sibling convention and being referenced viatype: "reference".
In short: spatial: remains the authoritative declaration of an affine
georeferencing matrix. coords: is the index-space → coordinate-space
router — it names the axes and tells readers where to look for each
axis's coordinate values, whether that is the affine transform, an
explicit array, an inline vector, or another convention.
The
multiscales
convention is domain-agnostic by design: each level's
transform.scale and transform.translation are per-axis arrays of length
equal to the array rank, not assumed to be spatial. A pyramid can
therefore downsample (or upsample) any dimension — temporal,
spectral, vertical, or domain-specific — not only spatial ones.
coords: slots in cleanly because each multiscales level is its own
Zarr array (or group) node, so each level independently carries:
- its Zarr v3
dimension_names, and - its own
coords:coordinatesmap.
Readers therefore route each level's axes through coords: exactly as
they would for a single-level array. The two conventions stay orthogonal:
multiscales describes the resampling relationship between levels;
coords: describes the index-to-coordinate mapping within each level.
The group declares the pyramid layout; each level array declares its own
dimension names and coords:coordinates. The descriptor path values
inside coords:coordinates are level-local — typically each level has its
own coordinate arrays sized to that level.
my_cube/ # group: zarr_conventions = [multiscales]
├── 0/ # native resolution
│ ├── data # array: dimension_names=[time,y,x], coords:coordinates={...}
│ ├── time # array: 1-D time coordinate at native step
│ ├── y # array: 1-D y coordinate
│ └── x # array: 1-D x coordinate
├── 1/ # half-res in y/x, 7-day temporal aggregation
│ ├── data
│ ├── time # 1-D time coordinate at weekly step
│ ├── y
│ └── x
└── 2/ # quarter-res in y/x, monthly temporal aggregation
├── data
├── time
├── y
└── x
Group node (my_cube/zarr.json):
{
"zarr_format": 3,
"node_type": "group",
"attributes": {
"zarr_conventions": [
{ "name": "multiscales", "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json" }
],
"multiscales": {
"layout": [
{ "asset": "0/data", "transform": { "scale": [1, 1, 1] } },
{ "asset": "1/data", "derived_from": "0/data", "transform": { "scale": [7, 2, 2] } },
{ "asset": "2/data", "derived_from": "1/data", "transform": { "scale": [4, 2, 2] } }
],
"resampling_method": "mean"
}
}
}The transform.scale arrays carry one entry per dimension. The first
slot (7, then 4) is the temporal downsampling factor — daily → weekly
→ monthly — exactly the same machinery that downsamples y/x.
Native-resolution level array (my_cube/0/data/zarr.json):
{
"zarr_format": 3,
"node_type": "array",
"dimension_names": ["time", "y", "x"],
"attributes": {
"zarr_conventions": [
{ "name": "coords", "schema_url": "https://raw.githubusercontent.com/zarr-conventions/coords/refs/tags/v1/schema.json" }
],
"coords:coordinates": {
"time": { "type": "array", "path": "../time", "standard_name": "time", "units": "days since 2020-01-01", "axis": "T" },
"y": { "type": "reference", "convention": "spatial", "axis": "Y" },
"x": { "type": "reference", "convention": "spatial", "axis": "X" }
}
}
}Downsampled level (my_cube/1/data/zarr.json) is structurally identical
but its sibling ../time array holds the weekly-step values, and its
spatial:transform (declared on the same level node) reflects the 2× y/x
scaling:
{
"zarr_format": 3,
"node_type": "array",
"dimension_names": ["time", "y", "x"],
"attributes": {
"zarr_conventions": [
{ "name": "coords", "schema_url": "https://raw.githubusercontent.com/zarr-conventions/coords/refs/tags/v1/schema.json" }
],
"coords:coordinates": {
"time": { "type": "array", "path": "../time", "units": "days since 2020-01-01", "axis": "T" },
"y": { "type": "reference", "convention": "spatial", "axis": "Y" },
"x": { "type": "reference", "convention": "spatial", "axis": "X" }
}
}
}Notes on this composition:
- Per-level coordinate independence. Each level keeps its own
coords:coordinates, so coordinates can change shape, path, or even representation type across levels (e.g. a level with explicittimearray, a coarser level with aninlinetime vector). - Non-spatial pyramids work too. Nothing in
coords:or inmultiscalesrequires a spatial axis. A pure(time, band)cube can carry a(time-aggregation, band-thinning)pyramid by setting the per-axistransform.scaleentries accordingly. - Mixed downsampling factors are first-class. The
transform.scalevector can use1for axes that are not downsampled at a given level — e.g.[1, 2, 2]downsamples onlyy/x, leavingtimeuntouched.
See the examples directory for complete Zarr convention metadata examples:
- examples/coords.json — minimal example mixing an
explicit time coordinate array with affine
y/xaxes delegated to thespatialconvention. - examples/coords-interval.json — implicit
axes using
type: "interval": an ISO 8601 daily time interval plus numeric elevation and azimuth axes. - examples/coords-point-cloud.json —
auxiliary coordinates over a
sampleindex dimension (lat(sample),lon(sample),time(sample)) usingtype: "array"withindexed_by. - examples/coords-curvilinear.json —
multi-dimensional coordinates
lat(y, x)/lon(y, x)on a curvilinear grid, each keyed by name withindexed_by: ["y", "x"].
This convention follows the integer-major with URL pin contract (contract #4 in the Zarr Conventions Guidance Implementation Contracts):
- The
schema_urlandspec_urlcarry the integer major version (/refs/tags/v1/...,/blob/v1/...). - All v1.x changes are additive: new optional fields, new
typevalues in the coordinate descriptor enum, and broadened ranges only. - Breaking changes (renaming, retyping, removing, or semantically shifting
existing fields) require a new major: tag
v2and publish a fresh schema under/refs/tags/v2/schema.json. - Readers SHOULD tolerate unknown
typevalues in coordinate descriptors and unknown additional fields per the conventions framework's safely-ignorable principle.
This convention is specified against Zarr v3. Zarr v2 datasets can use it with two adaptations, both already established in the Zarr / Xarray ecosystem:
-
Dimension names live in attributes, not at the top level. Zarr v2 has no
dimension_namesfield. The de-facto equivalent — established by Xarray — is the_ARRAY_DIMENSIONSattribute on each array, an ordered list of strings stored under.zattrs. Implementations consumingcoords:on a Zarr v2 array MUST treat_ARRAY_DIMENSIONSas the source of dimension names thatcoords:coordinateskeys reference. -
Convention metadata location is unchanged. The
zarr_conventionsregistration array and thecoords:coordinates/coords:versionfields live in the same attribute location as in v3 (i.e. under.zattrsrather than under theattributeskey of a v3 array's metadata document).
Side-by-side:
| Concept | Zarr v3 | Zarr v2 (with Xarray convention) |
|---|---|---|
| Dimension names | Top-level dimension_names field on array metadata |
_ARRAY_DIMENSIONS attribute in .zattrs |
| Convention registry | attributes.zarr_conventions |
.zattrs.zarr_conventions |
coords:coordinates |
attributes["coords:coordinates"] |
.zattrs["coords:coordinates"] |
Descriptor path |
Zarr-relative path to a sibling array | Same |
Auxiliary indexed_by |
Dimension names from dimension_names |
Dimension names from _ARRAY_DIMENSIONS |
Everything else in this specification — the four descriptor type values,
the composition with spatial / proj / multiscales, the
out-of-scope status of CF semantics — applies unchanged to Zarr v2.
The JSON Schema in this repository validates Zarr v3 metadata documents.
Validating Zarr v2 datasets is left to v2-aware tooling: a v2 validator
would synthesize an equivalent document from the v2 array's .zarray and
.zattrs files and then apply the same schema.
The template is based on the STAC extensions template.
Coordinate-array semantics are inspired by the
CF conventions and the Xarray data model.
The affine spatial composition path delegates to the
Zarr spatial convention
via a reference descriptor.