Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
8da1333
chore(deps): bump the actions group across 1 directory with 8 updates…
dependabot[bot] May 31, 2026
659c734
Merge branch 'main' of https://github.com/d-v-b/zarr-python
d-v-b Jun 6, 2026
51c994b
Merge branch 'main' of https://github.com/zarr-developers/zarr-python
d-v-b Jun 6, 2026
7732db3
Merge branch 'main' of https://github.com/zarr-developers/zarr-python
d-v-b Jun 7, 2026
913b41b
Merge branch 'main' of https://github.com/zarr-developers/zarr-python
d-v-b Jun 9, 2026
117b7ba
Merge branch 'main' of github.com:d-v-b/zarr-python
d-v-b Jun 12, 2026
d4de75d
Merge branch 'main' of github.com:zarr-developers/zarr-python
d-v-b Jun 12, 2026
86dabd5
Merge branch 'main' of github.com:zarr-developers/zarr-python
d-v-b Jun 12, 2026
a2e6002
Merge branch 'main' of https://github.com/d-v-b/zarr-python
d-v-b Jun 12, 2026
1621e1d
Merge branch 'main' of https://github.com/zarr-developers/zarr-python
d-v-b Jun 16, 2026
db473cd
Merge branch 'main' of https://github.com/zarr-developers/zarr-python
d-v-b Jun 25, 2026
dca5641
Merge branch 'main' of https://github.com/d-v-b/zarr-python
d-v-b Jun 30, 2026
a399213
Merge branch 'main' of https://github.com/zarr-developers/zarr-python
d-v-b Jul 1, 2026
ce2cfd7
feat(zarr-metadata): add model._validation — structural validators
d-v-b Jul 4, 2026
eee768b
feat(zarr-metadata): add model._array — array metadata models
d-v-b Jul 5, 2026
6b39147
test(zarr-metadata): port array model test suite
d-v-b Jul 5, 2026
9f32f14
feat(zarr-metadata): add group metadata models
d-v-b Jul 5, 2026
a85319f
test(zarr-metadata): add consolidated metadata model tests
d-v-b Jul 5, 2026
fbfb425
feat(zarr-metadata): export model layer from package front door
d-v-b Jul 5, 2026
99608ce
docs(zarr-metadata): changelog entry for the model layer
d-v-b Jul 5, 2026
c98a122
feat(zarr-metadata): harden model validation and error reporting
d-v-b Jul 5, 2026
be736da
feat(zarr-metadata): annotate metadata fields with role alias Metadat…
d-v-b Jul 5, 2026
7fc0211
test(zarr-metadata): assert required-key coverage via the typed constant
d-v-b Jul 5, 2026
15e0218
test(zarr-metadata): single whole-document comparison for v3 to_json
d-v-b Jul 5, 2026
c0a40cf
fix(zarr-metadata): close validation holes found by adversarial review
d-v-b Jul 5, 2026
b6e53ce
feat(zarr-metadata): expose must_understand_fields on the v3 models
d-v-b Jul 5, 2026
22188c9
test(zarr-metadata): executable example of pydantic integration
d-v-b Jul 5, 2026
6ea1533
test(zarr-metadata): document pydantic's native dataclass path and wh…
d-v-b Jul 5, 2026
03ee0b1
test(zarr-metadata): engine-backed pydantic BaseModel example (pydant…
d-v-b Jul 5, 2026
9193bf4
test(zarr-metadata): pin that a null dimension_names field is invalid
d-v-b Jul 5, 2026
eaf2587
feat(zarr-metadata): optional pydantic integration as zarr_metadata.p…
d-v-b Jul 5, 2026
bd80d7d
fix(zarr-metadata): create_default derives the chunk grid from shape
d-v-b Jul 5, 2026
bdec803
test(zarr-metadata): pin zero-length-dimension case of the derived ch…
d-v-b Jul 5, 2026
965ea02
docs(zarr-metadata): document that create_default's derivation is one…
d-v-b Jul 5, 2026
d4a0f8e
refactor(zarr-metadata): eliminate every type-ignore comment
d-v-b Jul 5, 2026
0a7f674
fix(zarr-metadata): absent v2 dimension_separator means '.', not '/'
d-v-b Jul 5, 2026
69ae901
feat(zarr-metadata): UNSET sentinel for absent optional document keys
d-v-b Jul 5, 2026
672d2bf
fix(zarr-metadata): accept and preserve the wild consolidated_metadat…
d-v-b Jul 5, 2026
fa7b7cf
fix(zarr-metadata): repair consolidated_metadata null to absence, not…
d-v-b Jul 5, 2026
da3094d
docs(zarr-metadata): pin the Sentinel blocker to pyright regression #…
d-v-b Jul 5, 2026
a9edd1f
docs(zarr-metadata): sentinel switch is blocked by mypy too, not just…
d-v-b Jul 5, 2026
934095e
docs(zarr-metadata): PEP 661 is Final (Python 3.15), not a draft
d-v-b Jul 5, 2026
800546a
docs(zarr-metadata): ty fully supports typed sentinels; pyright/mypy …
d-v-b Jul 5, 2026
f1fba41
feat(zarr-metadata): adopt the PEP 661 sentinel for UNSET
d-v-b Jul 5, 2026
892868c
Merge branch 'main' into zarr-metadata-model-layer
d-v-b Jul 5, 2026
8156ac0
fix(zarr-metadata): .zattrs presence is part of the store, not an art…
d-v-b Jul 5, 2026
1887aa8
Merge branch 'main' into zarr-metadata-model-layer
d-v-b Jul 6, 2026
04f777d
Merge branch 'main' into zarr-metadata-model-layer
d-v-b Jul 6, 2026
340a17d
fix(zarr-metadata): require typing_extensions>=4.16 so UNSET pickles …
d-v-b Jul 8, 2026
06668a7
refactor(zarr-metadata): format-version-first naming; dataclasses pri…
d-v-b Jul 8, 2026
91dd973
Merge branch 'main' into zarr-metadata-model-layer
d-v-b Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/zarr-metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ jobs:
- name: Sync test dependency group
run: uv sync --group test --python 3.11
- name: Run pyright
run: uv run --group test --with pyright pyright src
# Pinned to the last version that types PEP 661 sentinels in class
# attributes correctly; 1.1.405+ regressed (microsoft/pyright#11115).
# Unpin when the fix lands.
run: uv run --group test --with 'pyright==1.1.404' pyright src

zarr-metadata-complete:
name: zarr-metadata complete
Expand Down
4 changes: 2 additions & 2 deletions packages/zarr-metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ Zarr metadata. Pair them with a runtime validator like
```python
import json
from pydantic import TypeAdapter
from zarr_metadata.v3.array import ArrayMetadataV3
from zarr_metadata.v3.array import ZarrV3ArrayMetadataJSON

with open("zarr.json", "rb") as f:
raw = json.load(f)

metadata = TypeAdapter(ArrayMetadataV3).validate_python(raw)
metadata = TypeAdapter(ZarrV3ArrayMetadataJSON).validate_python(raw)
```

## What this is *not*
Expand Down
69 changes: 69 additions & 0 deletions packages/zarr-metadata/changes/210.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
Added `zarr_metadata.model`: frozen-dataclass models (`ZarrV2ArrayMetadata`,
`ZarrV3ArrayMetadata`, `ZarrV2GroupMetadata`, `ZarrV3GroupMetadata`,
`ZarrV2ConsolidatedMetadata`, `ZarrV3ConsolidatedMetadata`, `ZarrV3NamedConfig`)
that are canonical, lossless representations of Zarr metadata documents, plus
structural validators (`validate_*` / `is_*` / `parse_*`). Every v3 extension
point (data type, chunk grid, chunk key encoding, codecs, storage transformers)
is held as a name + configuration pair; nothing is interpreted. Model fields
are annotated with the role alias `ZarrV3MetadataField` (today exactly
`ZarrV3NamedConfig`), so the annotations convey the logical meaning of the
field and stay put if the spec ever adds a new field form.

Validation is strict about what the types declare: v2 `dtype` / `order` /
`compressor` / `filters` / `dimension_separator` shapes and the fixed
`zarr_format` / `node_type` literals are all enforced. Every
`ValidationProblem` carries a machine-readable `kind`
(`missing_key` / `invalid_type` / `invalid_value` / `invalid_json`) so
consumers can dispatch on the failure mode without matching message strings,
and every ingestion failure — including missing store keys and undecodable
bytes in `from_key_value` — surfaces as `MetadataValidationError`. An
adversarial review added further structural checks: JSON booleans are not
accepted as dimension lengths, dimensions are non-negative,
`dimension_names` must have one entry per dimension of `shape`, `attributes`
and `configuration` values are JSON-checked recursively (like `fill_value`),
and the inline consolidated-metadata envelope and entries are deep-validated
so the group validator's verdict always agrees with the model constructor.

The v3 models expose `must_understand_fields`: the subset of `extra_fields`
not explicitly waived with `must_understand: false` (fields are implicitly
must-understand per the spec). Readers discharge the spec's fail-to-open
duty by subtracting the extension names they recognize; the model only
partitions by obligation, since recognition is reader-specific.

Optional pydantic integration ships as `zarr_metadata.pydantic` (importing it
requires pydantic v2; the core package does not depend on it): one `Annotated`
field type per model, validating raw documents through `from_json`, passing
core-model instances through unchanged, and serializing via `to_json`. The
instances are the core model classes, so values interoperate freely with
non-pydantic code.

`create_default` keeps its output self-consistent: overriding `shape` without
a chunk grid derives one regular chunk covering the array (v3
`chunk_shape == shape`; v2 `chunks == shape`) instead of silently keeping the
scalar default's 0-d grid.

A v2 `.zarray` that omits `dimension_separator` is interpreted with the v2
convention's default `"."` (the model previously normalized absence to `"/"`,
which would misaddress the chunks of real-world default-separator arrays).
The value is never null: absent, `"."`, or `"/"` are the only spellings.

Optional document keys use `UNSET` — a PEP 661 sentinel
(`typing_extensions.Sentinel`), usable directly in type expressions — never
`None`: in a model, `None` always corresponds to a JSON `null` in the
document (a v2 `compressor`, an unnamed dimension inside `dimension_names`),
and `UNSET` always means the key is absent. Checker note: ty types the
sentinel exactly; pyright needs `<= 1.1.404` until microsoft/pyright#11115
is fixed (this package's CI pins it); mypy users need a `cast` or
`type: ignore` at narrowing sites until python/mypy#21647 merges. This keeps semantically distinct spellings
distinct — an absent `dimension_names` ("there are no dimension names") and
an explicit `[null, null]` ("every dimension has a name, which is null") are
different documents and round-trip as such. The `consolidated_metadata: null`
written by a historical zarr-python bug is the one deliberate exception to
faithful round-tripping: those stores remain readable, but the bug spelling
is repaired to absence on read and never written back.

The v2 models treat the `.zattrs` file's presence as part of the store:
`attributes` is `UNSET` when no `.zattrs` file exists (and `to_key_value`
emits none), while an explicit empty `.zattrs` is `{}` and round-trips as a
file. Previously `to_key_value` always emitted `.zattrs`, silently adding a
file to stores that never had one.
23 changes: 23 additions & 0 deletions packages/zarr-metadata/changes/4119.removal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
The document (TypedDict) types are renamed to put the format version at the
front of the name and to mark the JSON-document form with a `JSON` suffix,
so a format version can never be misread as a class revision and the bare
entity names are reserved for the `zarr_metadata.model` dataclasses:

- `ArrayMetadataV2` → `ZarrV2ArrayMetadataJSON` (and `...Partial` accordingly)
- `ArrayMetadataV3` → `ZarrV3ArrayMetadataJSON` (and `...Partial` accordingly)
- `GroupMetadataV2` → `ZarrV2GroupMetadataJSON` (and `...Partial` accordingly)
- `GroupMetadataV3` → `ZarrV3GroupMetadataJSON` (and `...Partial` accordingly)
- `ConsolidatedMetadataV2` → `ZarrV2ConsolidatedMetadataJSON`
- `ConsolidatedMetadataV3` → `ZarrV3ConsolidatedMetadataJSON`
- `NamedConfigV3` → `ZarrV3NamedConfigJSON`
- `MetadataV3` → `ZarrV3MetadataFieldJSON` (the union of the bare-name and
named-configuration spellings of one metadata field)
- `ExtensionFieldV3` → `ZarrV3ExtensionField`
- `CodecMetadataV2` → `ZarrV2CodecMetadata`
- `DataTypeMetadataV2` → `ZarrV2DataTypeMetadata`
- `ArrayOrderV2` → `ZarrV2ArrayOrder`
- `ArrayDimensionSeparatorV2` → `ZarrV2ArrayDimensionSeparator`

The old names are removed, not aliased. The `zarr_metadata.pydantic` field
types take the bare entity names (`ZarrV3ArrayMetadata`, ...), matching the
model classes they validate into.
9 changes: 8 additions & 1 deletion packages/zarr-metadata/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ classifiers = [
]
keywords = ["zarr"]
dependencies = [
"typing_extensions>=4.13",
# >=4.16: first release where `Sentinel` pickles by reference
# (`__reduce__` returns the sentinel's name), so `UNSET` — and any model
# holding it — can cross process boundaries and be deep-copied with its
# singleton identity intact. 4.15 and earlier refuse to pickle sentinels.
"typing_extensions>=4.16",
]

[project.urls]
Expand Down Expand Up @@ -82,6 +86,9 @@ checks = [
"PR06",
]

# CI pins pyright==1.1.404: later versions regress PEP 661 sentinel typing in
# class attributes (microsoft/pyright#11115), which zarr_metadata.model._sentinel
# relies on. Use the same pin locally; unpin when the fix lands.
[tool.pyright]
include = ["src"]
enableExperimentalFeatures = true
Expand Down
102 changes: 72 additions & 30 deletions packages/zarr-metadata/src/zarr_metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
from importlib.metadata import version

from zarr_metadata._common import JSONValue, NamedConfigV3
from zarr_metadata._common import JSONValue, ZarrV3NamedConfigJSON
from zarr_metadata.model import (
UNSET,
MetadataValidationError,
ProblemKind,
ValidationProblem,
ZarrV2ArrayMetadata,
ZarrV2ArrayMetadataPartial,
ZarrV2ConsolidatedMetadata,
ZarrV2GroupMetadata,
ZarrV2GroupMetadataPartial,
ZarrV3ArrayMetadata,
ZarrV3ArrayMetadataPartial,
ZarrV3ConsolidatedMetadata,
ZarrV3GroupMetadata,
ZarrV3GroupMetadataPartial,
ZarrV3MetadataField,
ZarrV3NamedConfig,
)
from zarr_metadata.v2.array import (
ARRAY_DIMENSION_SEPARATOR_V2,
ARRAY_ORDER_V2,
ArrayDimensionSeparatorV2,
ArrayMetadataV2,
ArrayMetadataV2Partial,
ArrayOrderV2,
DataTypeMetadataV2,
ZArrayMetadata,
ZarrV2ArrayDimensionSeparator,
ZarrV2ArrayMetadataJSON,
ZarrV2ArrayMetadataJSONPartial,
ZarrV2ArrayOrder,
ZarrV2DataTypeMetadata,
)
from zarr_metadata.v2.attributes import ZAttrsMetadata
from zarr_metadata.v2.codec import CodecMetadataV2
from zarr_metadata.v2.consolidated import ConsolidatedMetadataV2
from zarr_metadata.v2.group import GroupMetadataV2, GroupMetadataV2Partial, ZGroupMetadata
from zarr_metadata.v3._common import MetadataV3
from zarr_metadata.v3.array import ArrayMetadataV3, ArrayMetadataV3Partial, ExtensionFieldV3
from zarr_metadata.v2.codec import ZarrV2CodecMetadata
from zarr_metadata.v2.consolidated import ZarrV2ConsolidatedMetadataJSON
from zarr_metadata.v2.group import (
ZarrV2GroupMetadataJSON,
ZarrV2GroupMetadataJSONPartial,
ZGroupMetadata,
)
from zarr_metadata.v3._common import ZarrV3MetadataFieldJSON
from zarr_metadata.v3.array import (
ZarrV3ArrayMetadataJSON,
ZarrV3ArrayMetadataJSONPartial,
ZarrV3ExtensionField,
)
from zarr_metadata.v3.chunk_grid.rectilinear import (
RECTILINEAR_CHUNK_GRID_NAME,
RectilinearChunkGridMetadata,
Expand Down Expand Up @@ -86,7 +112,7 @@
TransposeCodecName,
)
from zarr_metadata.v3.codec.zstd import ZSTD_CODEC_NAME, ZstdCodecMetadata, ZstdCodecName
from zarr_metadata.v3.consolidated import ConsolidatedMetadataV3
from zarr_metadata.v3.consolidated import ZarrV3ConsolidatedMetadataJSON
from zarr_metadata.v3.data_type.bool import (
BOOL_DATA_TYPE_NAME,
BoolDataTypeName,
Expand Down Expand Up @@ -185,7 +211,7 @@
Uint64DataTypeName,
Uint64FillValue,
)
from zarr_metadata.v3.group import GroupMetadataV3, GroupMetadataV3Partial
from zarr_metadata.v3.group import ZarrV3GroupMetadataJSON, ZarrV3GroupMetadataJSONPartial

__version__ = version("zarr-metadata")

Expand Down Expand Up @@ -231,15 +257,10 @@
"UINT16_DATA_TYPE_NAME",
"UINT32_DATA_TYPE_NAME",
"UINT64_DATA_TYPE_NAME",
"UNSET",
"V2_CHUNK_KEY_ENCODING_NAME",
"V2_CHUNK_KEY_ENCODING_SEPARATOR",
"ZSTD_CODEC_NAME",
"ArrayDimensionSeparatorV2",
"ArrayMetadataV2",
"ArrayMetadataV2Partial",
"ArrayMetadataV3",
"ArrayMetadataV3Partial",
"ArrayOrderV2",
"BloscCName",
"BloscCodecMetadata",
"BloscCodecName",
Expand All @@ -254,31 +275,22 @@
"CastRoundingMode",
"CastValueCodecMetadata",
"CastValueCodecName",
"CodecMetadataV2",
"Complex64DataTypeName",
"Complex64FillValue",
"Complex128DataTypeName",
"Complex128FillValue",
"ConsolidatedMetadataV2",
"ConsolidatedMetadataV3",
"Crc32cCodecMetadata",
"Crc32cCodecName",
"DataTypeMetadataV2",
"DefaultChunkKeyEncodingMetadata",
"DefaultChunkKeyEncodingName",
"DefaultChunkKeyEncodingSeparator",
"Endianness",
"ExtensionFieldV3",
"Float16DataTypeName",
"Float16FillValue",
"Float32DataTypeName",
"Float32FillValue",
"Float64DataTypeName",
"Float64FillValue",
"GroupMetadataV2",
"GroupMetadataV2Partial",
"GroupMetadataV3",
"GroupMetadataV3Partial",
"GzipCodecMetadata",
"GzipCodecName",
"Int8DataTypeName",
Expand All @@ -290,13 +302,13 @@
"Int64DataTypeName",
"Int64FillValue",
"JSONValue",
"MetadataV3",
"NamedConfigV3",
"MetadataValidationError",
"NumpyDatetime64DataTypeName",
"NumpyDatetime64FillValue",
"NumpyTimeUnit",
"NumpyTimedelta64DataTypeName",
"NumpyTimedelta64FillValue",
"ProblemKind",
"RawBytesDataTypeName",
"RawBytesFillValue",
"RectilinearChunkGridMetadata",
Expand Down Expand Up @@ -325,9 +337,39 @@
"V2ChunkKeyEncodingMetadata",
"V2ChunkKeyEncodingName",
"V2ChunkKeyEncodingSeparator",
"ValidationProblem",
"ZArrayMetadata",
"ZAttrsMetadata",
"ZGroupMetadata",
"ZarrV2ArrayDimensionSeparator",
"ZarrV2ArrayMetadata",
"ZarrV2ArrayMetadataJSON",
"ZarrV2ArrayMetadataJSONPartial",
"ZarrV2ArrayMetadataPartial",
"ZarrV2ArrayOrder",
"ZarrV2CodecMetadata",
"ZarrV2ConsolidatedMetadata",
"ZarrV2ConsolidatedMetadataJSON",
"ZarrV2DataTypeMetadata",
"ZarrV2GroupMetadata",
"ZarrV2GroupMetadataJSON",
"ZarrV2GroupMetadataJSONPartial",
"ZarrV2GroupMetadataPartial",
"ZarrV3ArrayMetadata",
"ZarrV3ArrayMetadataJSON",
"ZarrV3ArrayMetadataJSONPartial",
"ZarrV3ArrayMetadataPartial",
"ZarrV3ConsolidatedMetadata",
"ZarrV3ConsolidatedMetadataJSON",
"ZarrV3ExtensionField",
"ZarrV3GroupMetadata",
"ZarrV3GroupMetadataJSON",
"ZarrV3GroupMetadataJSONPartial",
"ZarrV3GroupMetadataPartial",
"ZarrV3MetadataField",
"ZarrV3MetadataFieldJSON",
"ZarrV3NamedConfig",
"ZarrV3NamedConfigJSON",
"ZstdCodecMetadata",
"ZstdCodecName",
"__version__",
Expand Down
4 changes: 2 additions & 2 deletions packages/zarr-metadata/src/zarr_metadata/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

JSONValue = TypeAliasType(
"JSONValue",
"int | float | bool | None | str | list[JSONValue] | tuple[JSONValue, ...] | Mapping[str, JSONValue]", # type: ignore[reportInvalidTypeForm]
"int | float | bool | None | str | list[JSONValue] | tuple[JSONValue, ...] | Mapping[str, JSONValue]",
)
"""A recursive type alias for JSON-encodable values.
Expand All @@ -24,7 +24,7 @@
"""


class NamedConfigV3(TypedDict):
class ZarrV3NamedConfigJSON(TypedDict):
"""
Externally-tagged union member for a metadata field.
Expand Down
Loading
Loading