Skip to content

zarr_metadata.model

zarr_metadata.model

In-memory models for Zarr metadata documents.

Models are frozen dataclasses that hold a canonical, semantically lossless representation of the JSON documents; they never interpret extension points (codecs, chunk grids, data types). Validators check JSON structure, not domain validity. Each document concept gets a validate_* function returning every problem found (a list[ValidationProblem], each with a machine-readable kind), an is_* type guard, and a parse_* function that narrows or raises MetadataValidationError. Model from_json / from_key_value constructors raise MetadataValidationError for every ingestion failure, including missing store keys and undecodable bytes.

ARRAY_METADATA_OPTIONAL_KEYS_V3 module-attribute

ARRAY_METADATA_OPTIONAL_KEYS_V3: Final[frozenset[str]] = (
    frozenset(ZarrV3ArrayMetadataJSON.__optional_keys__)
)

ARRAY_METADATA_REQUIRED_KEYS_V2 module-attribute

ARRAY_METADATA_REQUIRED_KEYS_V2: Final[frozenset[str]] = (
    frozenset(ZarrV2ArrayMetadataJSON.__required_keys__)
)

ARRAY_METADATA_REQUIRED_KEYS_V3 module-attribute

ARRAY_METADATA_REQUIRED_KEYS_V3: Final[frozenset[str]] = (
    frozenset(ZarrV3ArrayMetadataJSON.__required_keys__)
)

ARRAY_METADATA_STANDARD_KEYS_V3 module-attribute

GROUP_METADATA_OPTIONAL_KEYS_V3 module-attribute

GROUP_METADATA_OPTIONAL_KEYS_V3: Final[frozenset[str]] = (
    frozenset(ZarrV3GroupMetadataJSON.__optional_keys__)
)

GROUP_METADATA_REQUIRED_KEYS_V2 module-attribute

GROUP_METADATA_REQUIRED_KEYS_V2: Final[frozenset[str]] = (
    frozenset(ZarrV2GroupMetadataJSON.__required_keys__)
)

GROUP_METADATA_REQUIRED_KEYS_V3 module-attribute

GROUP_METADATA_REQUIRED_KEYS_V3: Final[frozenset[str]] = (
    frozenset(ZarrV3GroupMetadataJSON.__required_keys__)
)

GROUP_METADATA_STANDARD_KEYS_V3 module-attribute

ProblemKind module-attribute

ProblemKind = Literal[
    "missing_key",
    "invalid_type",
    "invalid_value",
    "invalid_json",
]

Machine-readable classification of a ValidationProblem.

  • missing_key: a required key (document key or store key) is absent.
  • invalid_type: a value has the wrong structural type (e.g. a string where a mapping is required, a non-JSON-serializable object).
  • invalid_value: a value has an acceptable type but an invalid content (e.g. zarr_format: 2 in a v3 document, order: "Q").
  • invalid_json: bytes that do not decode as JSON.

UNSET module-attribute

UNSET = Sentinel('UNSET')

Marks a metadata-document key as absent (PEP 661 sentinel; usable directly in type expressions, e.g. tuple[str, ...] | UNSET). Test with is UNSET.

ZARR_V2_ARRAY_METADATA_STORE_KEY module-attribute

ZARR_V2_ARRAY_METADATA_STORE_KEY: Final[
    ZarrV2ArrayMetadataStoreKey
] = ".zarray"

The store key a v2 array's metadata document is persisted under.

ZARR_V2_ATTRIBUTES_STORE_KEY module-attribute

ZARR_V2_ATTRIBUTES_STORE_KEY: Final[
    ZarrV2AttributesStoreKey
] = ".zattrs"

The store key a v2 node's user attributes are persisted under.

Shared by arrays and groups: both node types keep their attributes in a sibling .zattrs file.

ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY module-attribute

ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY: Final[
    ZarrV2ConsolidatedMetadataStoreKey
] = ".zmetadata"

The store key a v2 hierarchy's consolidated metadata is persisted under.

Like the document it names, this is a reference-implementation convention rather than a spec artifact; see the module docstring.

ZARR_V2_GROUP_METADATA_STORE_KEY module-attribute

ZARR_V2_GROUP_METADATA_STORE_KEY: Final[
    ZarrV2GroupMetadataStoreKey
] = ".zgroup"

The store key a v2 group's metadata document is persisted under.

ZARR_V3_ARRAY_METADATA_STORE_KEY module-attribute

ZARR_V3_ARRAY_METADATA_STORE_KEY: Final[
    ZarrV3ArrayMetadataStoreKey
] = "zarr.json"

The store key a v3 array's metadata document is persisted under.

v3 uses one key for both node types; the document's node_type field distinguishes an array from a group.

ZARR_V3_CONSOLIDATED_METADATA_KEY module-attribute

ZARR_V3_CONSOLIDATED_METADATA_KEY: Final = (
    "consolidated_metadata"
)

The key under which consolidated metadata is embedded in a v3 group document.

Unlike the v2 .zmetadata file, this is not a store key: consolidated metadata is carried as an extension field inside the group's own zarr.json. Like its v2 counterpart it is a reference-implementation convention, not a spec artifact.

ZARR_V3_GROUP_METADATA_STORE_KEY module-attribute

ZARR_V3_GROUP_METADATA_STORE_KEY: Final[
    ZarrV3GroupMetadataStoreKey
] = "zarr.json"

The store key a v3 group's metadata document is persisted under.

v3 uses one key for both node types; the document's node_type field distinguishes a group from an array.

ZarrV2ArrayMetadataStoreKey module-attribute

ZarrV2ArrayMetadataStoreKey = Literal['.zarray']

Literal type of the store key holding a v2 array's metadata document.

ZarrV2AttributesStoreKey module-attribute

ZarrV2AttributesStoreKey = Literal['.zattrs']

Literal type of the store key holding a v2 node's user attributes.

ZarrV2ConsolidatedMetadataStoreKey module-attribute

ZarrV2ConsolidatedMetadataStoreKey = Literal['.zmetadata']

Literal type of the store key holding a v2 hierarchy's consolidated metadata.

ZarrV2GroupMetadataStoreKey module-attribute

ZarrV2GroupMetadataStoreKey = Literal['.zgroup']

Literal type of the store key holding a v2 group's metadata document.

ZarrV3ArrayMetadataStoreKey module-attribute

ZarrV3ArrayMetadataStoreKey = Literal['zarr.json']

Literal type of the store key holding a v3 array's metadata document.

ZarrV3GroupMetadataStoreKey module-attribute

ZarrV3GroupMetadataStoreKey = Literal['zarr.json']

Literal type of the store key holding a v3 group's metadata document.

ZarrV3MetadataField module-attribute

ZarrV3MetadataField: TypeAlias = ZarrV3NamedConfig

The in-memory model of one field of a v3 metadata document.

This is the role-named alias for annotation positions: model fields and consumer signatures should say ZarrV3MetadataField (the logical meaning) rather than ZarrV3NamedConfig (the serialized form the field currently takes). Today every metadata field normalizes to a named configuration plus its reader obligation, so the alias is exactly ZarrV3NamedConfig; if a future spec revision adds a field form that cannot be normalized to those values, this alias widens to a union and annotation sites do not change. Mirrors the raw-layer split between ZarrV3NamedConfigJSON (shape) and ZarrV3MetadataFieldJSON (field union).

__all__ module-attribute

__all__ = [
    "ARRAY_METADATA_OPTIONAL_KEYS_V3",
    "ARRAY_METADATA_REQUIRED_KEYS_V2",
    "ARRAY_METADATA_REQUIRED_KEYS_V3",
    "ARRAY_METADATA_STANDARD_KEYS_V3",
    "GROUP_METADATA_OPTIONAL_KEYS_V3",
    "GROUP_METADATA_REQUIRED_KEYS_V2",
    "GROUP_METADATA_REQUIRED_KEYS_V3",
    "GROUP_METADATA_STANDARD_KEYS_V3",
    "UNSET",
    "ZARR_V2_ARRAY_METADATA_STORE_KEY",
    "ZARR_V2_ATTRIBUTES_STORE_KEY",
    "ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY",
    "ZARR_V2_GROUP_METADATA_STORE_KEY",
    "ZARR_V3_ARRAY_METADATA_STORE_KEY",
    "ZARR_V3_CONSOLIDATED_METADATA_KEY",
    "ZARR_V3_GROUP_METADATA_STORE_KEY",
    "MetadataValidationError",
    "ProblemKind",
    "ValidationProblem",
    "ZarrV2ArrayMetadata",
    "ZarrV2ArrayMetadataPartial",
    "ZarrV2ArrayMetadataStoreKey",
    "ZarrV2AttributesStoreKey",
    "ZarrV2ConsolidatedMetadata",
    "ZarrV2ConsolidatedMetadataStoreKey",
    "ZarrV2GroupMetadata",
    "ZarrV2GroupMetadataPartial",
    "ZarrV2GroupMetadataStoreKey",
    "ZarrV3ArrayMetadata",
    "ZarrV3ArrayMetadataPartial",
    "ZarrV3ArrayMetadataStoreKey",
    "ZarrV3ConsolidatedMetadata",
    "ZarrV3GroupMetadata",
    "ZarrV3GroupMetadataPartial",
    "ZarrV3GroupMetadataStoreKey",
    "ZarrV3MetadataField",
    "ZarrV3NamedConfig",
    "is_array_metadata_v2",
    "is_array_metadata_v3",
    "is_group_metadata_v2",
    "is_group_metadata_v3",
    "is_json",
    "is_metadata_field_v3",
    "parse_array_metadata_v2",
    "parse_array_metadata_v3",
    "parse_group_metadata_v2",
    "parse_group_metadata_v3",
    "parse_json",
    "parse_metadata_field_v3",
    "validate_array_metadata_v2",
    "validate_array_metadata_v3",
    "validate_group_metadata_v2",
    "validate_group_metadata_v3",
    "validate_json",
    "validate_metadata_field_v3",
]

MetadataValidationError

Bases: ValueError

Raised when a value fails structural metadata validation.

Carries every problem found (not just the first) in .problems.

Source code in src/zarr_metadata/model/_validation.py
class MetadataValidationError(ValueError):
    """Raised when a value fails structural metadata validation.

    Carries every problem found (not just the first) in `.problems`.
    """

    def __init__(self, problems: list[ValidationProblem]) -> None:
        self.problems = problems
        super().__init__("\n".join(str(problem) for problem in problems))

problems instance-attribute

problems = problems

__init__

__init__(problems: list[ValidationProblem]) -> None
Source code in src/zarr_metadata/model/_validation.py
def __init__(self, problems: list[ValidationProblem]) -> None:
    self.problems = problems
    super().__init__("\n".join(str(problem) for problem in problems))

ValidationProblem dataclass

A single structural problem found while validating a metadata document.

loc is the path from the document root to the offending value, e.g. ("codecs", 0, "name"). An empty loc refers to the document as a whole. kind classifies the failure mode for programmatic dispatch; message is the human-readable description.

Source code in src/zarr_metadata/model/_validation.py
@dataclass(frozen=True, slots=True)
class ValidationProblem:
    """A single structural problem found while validating a metadata document.

    `loc` is the path from the document root to the offending value, e.g.
    `("codecs", 0, "name")`. An empty `loc` refers to the document as a whole.
    `kind` classifies the failure mode for programmatic dispatch; `message`
    is the human-readable description.
    """

    loc: tuple[str | int, ...]
    message: str
    kind: ProblemKind

    def __str__(self) -> str:
        location = ".".join(str(part) for part in self.loc) if self.loc else "<root>"
        return f"{location}: {self.message}"

kind instance-attribute

loc instance-attribute

loc: tuple[str | int, ...]

message instance-attribute

message: str

__init__

__init__(
    loc: tuple[str | int, ...],
    message: str,
    kind: ProblemKind,
) -> None

__str__

__str__() -> str
Source code in src/zarr_metadata/model/_validation.py
def __str__(self) -> str:
    location = ".".join(str(part) for part in self.loc) if self.loc else "<root>"
    return f"{location}: {self.message}"

ZarrV2ArrayMetadata dataclass

In-memory model of a v2 array metadata document.

A canonical, lossless representation of the .zarray content plus the sibling .zattrs attributes. dtype, compressor, and filters are held in their raw JSON forms and are never interpreted; fill_value is held verbatim in its JSON form. attributes is UNSET when no .zattrs file (or merged attributes key) exists — distinct from an explicit empty .zattrs, which is {} and round-trips as a file. One spelling normalization: a .zarray that omits dimension_separator means "." by the v2 convention, and the model holds and re-emits that value explicitly.

Source code in src/zarr_metadata/model/_array.py
@dataclass(frozen=True, slots=True, kw_only=True)
class ZarrV2ArrayMetadata:
    """In-memory model of a v2 array metadata document.

    A canonical, lossless representation of the `.zarray` content plus the
    sibling `.zattrs` attributes. `dtype`, `compressor`, and `filters` are
    held in their raw JSON forms and are never interpreted; `fill_value` is
    held verbatim in its JSON form. `attributes` is `UNSET` when no
    `.zattrs` file (or merged `attributes` key) exists — distinct from an
    explicit empty `.zattrs`, which is `{}` and round-trips as a file. One
    spelling normalization: a `.zarray` that omits `dimension_separator`
    means `"."` by the v2 convention, and the model holds and re-emits that
    value explicitly.
    """

    zarr_format: Literal[2] = field(default=2, init=False)
    shape: tuple[int, ...]
    dtype: ZarrV2DataTypeMetadata
    chunks: tuple[int, ...]
    fill_value: JSONValue
    order: ZarrV2ArrayOrder
    compressor: ZarrV2CodecMetadata | None
    filters: tuple[ZarrV2CodecMetadata, ...] | None
    # "." is the v2 convention's default for an ABSENT dimension_separator key;
    # from_json normalizes absence to it (a semantics-preserving spelling
    # normalization, like the v3 bare-string metadata-field form). The value
    # is never None: the document grammar has no null spelling for this field.
    dimension_separator: ZarrV2ArrayDimensionSeparator = field(default=".")
    attributes: dict[str, JSONValue] | UNSET

    def update(self, **kwargs: Unpack[ZarrV2ArrayMetadataPartial]) -> ZarrV2ArrayMetadata:
        """
        Return a new `ZarrV2ArrayMetadata` with the given fields updated.

        Only the constructor-settable fields listed in
        `ZarrV2ArrayMetadataPartial` can be updated; the fixed `zarr_format` is
        rejected at the type level. Each given field fully replaces its previous
        value.
        """
        return dataclasses.replace(self, **kwargs)

    @classmethod
    def create_default(cls, **overrides: Unpack[ZarrV2ArrayMetadataPartial]) -> ZarrV2ArrayMetadata:
        """
        Create a default (empty) v2 array metadata model, with optional overrides.

        The default is a structurally-valid scalar `uint8` (`"|u1"`) array — the
        array analog of `list()` returning `[]`. Any field can be overridden by
        keyword (the same fields accepted by `update`). Overriding `shape`
        without `chunks` derives `chunks` equal to `shape` (one chunk covering
        the array).

        The derivation is deliberately one-way, matching the v3 model:
        overriding `chunks` without `shape` keeps the scalar default
        `shape=()`, and consistency between the two is the caller's
        responsibility.
        """
        if "shape" in overrides and "chunks" not in overrides:
            overrides["chunks"] = tuple(overrides["shape"])
        default = cls(
            shape=(),
            dtype="|u1",
            chunks=(),
            fill_value=0,
            order="C",
            compressor=None,
            filters=None,
            attributes=UNSET,
        )
        return default.update(**overrides)

    def to_json(self) -> ZarrV2ArrayMetadataJSON:
        """Return the merged in-memory document form.

        `attributes` is included when set (even empty). This is not the
        on-disk `.zarray` content: a conforming `.zarray` must exclude
        `attributes` (they live in the sibling `.zattrs` file). Use
        `to_key_value` to produce the spec-conforming split for storage.
        """
        # to_json output shares no mutable state with the model: every value
        # that can hold a mutable container is deep-copied.
        out: ZarrV2ArrayMetadataJSON = {
            "zarr_format": self.zarr_format,
            "shape": self.shape,
            "dtype": self.dtype,
            "order": self.order,
            "chunks": self.chunks,
            "fill_value": copy.deepcopy(self.fill_value),
            "dimension_separator": self.dimension_separator,
            "compressor": copy.deepcopy(self.compressor),
            "filters": copy.deepcopy(self.filters),
        }
        if self.attributes is not UNSET:
            out["attributes"] = copy.deepcopy(self.attributes)
        return out

    @classmethod
    def from_json(cls, data: object) -> ZarrV2ArrayMetadata:
        parsed = parse_array_metadata_v2(arrays_to_tuples(data))
        return cls(
            shape=parsed["shape"],
            dtype=parsed["dtype"],
            chunks=parsed["chunks"],
            fill_value=parsed["fill_value"],
            order=parsed["order"],
            compressor=parsed["compressor"],
            filters=parsed["filters"],
            dimension_separator=parsed.get("dimension_separator", "."),
            attributes=(dict(parsed["attributes"]) if "attributes" in parsed else UNSET),
        )

    @classmethod
    def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV2ArrayMetadata:
        zarray_raw = cast("object", load_store_json(mapping, ZARR_V2_ARRAY_METADATA_STORE_KEY))
        if not isinstance(zarray_raw, Mapping):
            return cls.from_json(zarray_raw)
        zarray = cast("Mapping[str, object]", zarray_raw)
        if "attributes" in zarray:
            raise MetadataValidationError(
                [
                    ValidationProblem(
                        ("attributes",),
                        "unexpected document member",
                        "invalid_value",
                    )
                ]
            )
        if ZARR_V2_ATTRIBUTES_STORE_KEY in mapping:
            zattrs = cast("object", load_store_json(mapping, ZARR_V2_ATTRIBUTES_STORE_KEY))
            return cls.from_json({**zarray, "attributes": zattrs})
        return cls.from_json(zarray)

    def to_key_value(
        self, *, indent: int | str | None = None
    ) -> Mapping[ZarrV2ArrayMetadataStoreKey | ZarrV2AttributesStoreKey, bytes]:
        # Attributes live only in the sibling `.zattrs` file; the `.zarray`
        # document must exclude them. The `.zattrs` key is present exactly
        # when attributes are set (even empty) — UNSET emits no file.
        zarray = {k: v for k, v in self.to_json().items() if k != "attributes"}
        out: dict[ZarrV2ArrayMetadataStoreKey | ZarrV2AttributesStoreKey, bytes] = {
            ZARR_V2_ARRAY_METADATA_STORE_KEY: dump_store_json(zarray, indent=indent)
        }
        if self.attributes is not UNSET:
            out[ZARR_V2_ATTRIBUTES_STORE_KEY] = dump_store_json(self.attributes, indent=indent)
        return out

attributes instance-attribute

attributes: dict[str, JSONValue] | UNSET

chunks instance-attribute

chunks: tuple[int, ...]

compressor instance-attribute

compressor: ZarrV2CodecMetadata | None

dimension_separator class-attribute instance-attribute

dimension_separator: ZarrV2ArrayDimensionSeparator = field(
    default="."
)

dtype instance-attribute

fill_value instance-attribute

fill_value: JSONValue

filters instance-attribute

filters: tuple[ZarrV2CodecMetadata, ...] | None

order instance-attribute

shape instance-attribute

shape: tuple[int, ...]

zarr_format class-attribute instance-attribute

zarr_format: Literal[2] = field(default=2, init=False)

__init__

__init__(
    *,
    shape: tuple[int, ...],
    dtype: ZarrV2DataTypeMetadata,
    chunks: tuple[int, ...],
    fill_value: JSONValue,
    order: ZarrV2ArrayOrder,
    compressor: ZarrV2CodecMetadata | None,
    filters: tuple[ZarrV2CodecMetadata, ...] | None,
    dimension_separator: ZarrV2ArrayDimensionSeparator = ".",
    attributes: dict[str, JSONValue] | UNSET,
) -> None

create_default classmethod

create_default(
    **overrides: Unpack[ZarrV2ArrayMetadataPartial],
) -> ZarrV2ArrayMetadata

Create a default (empty) v2 array metadata model, with optional overrides.

The default is a structurally-valid scalar uint8 ("|u1") array — the array analog of list() returning []. Any field can be overridden by keyword (the same fields accepted by update). Overriding shape without chunks derives chunks equal to shape (one chunk covering the array).

The derivation is deliberately one-way, matching the v3 model: overriding chunks without shape keeps the scalar default shape=(), and consistency between the two is the caller's responsibility.

Source code in src/zarr_metadata/model/_array.py
@classmethod
def create_default(cls, **overrides: Unpack[ZarrV2ArrayMetadataPartial]) -> ZarrV2ArrayMetadata:
    """
    Create a default (empty) v2 array metadata model, with optional overrides.

    The default is a structurally-valid scalar `uint8` (`"|u1"`) array — the
    array analog of `list()` returning `[]`. Any field can be overridden by
    keyword (the same fields accepted by `update`). Overriding `shape`
    without `chunks` derives `chunks` equal to `shape` (one chunk covering
    the array).

    The derivation is deliberately one-way, matching the v3 model:
    overriding `chunks` without `shape` keeps the scalar default
    `shape=()`, and consistency between the two is the caller's
    responsibility.
    """
    if "shape" in overrides and "chunks" not in overrides:
        overrides["chunks"] = tuple(overrides["shape"])
    default = cls(
        shape=(),
        dtype="|u1",
        chunks=(),
        fill_value=0,
        order="C",
        compressor=None,
        filters=None,
        attributes=UNSET,
    )
    return default.update(**overrides)

from_json classmethod

from_json(data: object) -> ZarrV2ArrayMetadata
Source code in src/zarr_metadata/model/_array.py
@classmethod
def from_json(cls, data: object) -> ZarrV2ArrayMetadata:
    parsed = parse_array_metadata_v2(arrays_to_tuples(data))
    return cls(
        shape=parsed["shape"],
        dtype=parsed["dtype"],
        chunks=parsed["chunks"],
        fill_value=parsed["fill_value"],
        order=parsed["order"],
        compressor=parsed["compressor"],
        filters=parsed["filters"],
        dimension_separator=parsed.get("dimension_separator", "."),
        attributes=(dict(parsed["attributes"]) if "attributes" in parsed else UNSET),
    )

from_key_value classmethod

from_key_value(
    mapping: Mapping[str, bytes],
) -> ZarrV2ArrayMetadata
Source code in src/zarr_metadata/model/_array.py
@classmethod
def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV2ArrayMetadata:
    zarray_raw = cast("object", load_store_json(mapping, ZARR_V2_ARRAY_METADATA_STORE_KEY))
    if not isinstance(zarray_raw, Mapping):
        return cls.from_json(zarray_raw)
    zarray = cast("Mapping[str, object]", zarray_raw)
    if "attributes" in zarray:
        raise MetadataValidationError(
            [
                ValidationProblem(
                    ("attributes",),
                    "unexpected document member",
                    "invalid_value",
                )
            ]
        )
    if ZARR_V2_ATTRIBUTES_STORE_KEY in mapping:
        zattrs = cast("object", load_store_json(mapping, ZARR_V2_ATTRIBUTES_STORE_KEY))
        return cls.from_json({**zarray, "attributes": zattrs})
    return cls.from_json(zarray)

to_json

Return the merged in-memory document form.

attributes is included when set (even empty). This is not the on-disk .zarray content: a conforming .zarray must exclude attributes (they live in the sibling .zattrs file). Use to_key_value to produce the spec-conforming split for storage.

Source code in src/zarr_metadata/model/_array.py
def to_json(self) -> ZarrV2ArrayMetadataJSON:
    """Return the merged in-memory document form.

    `attributes` is included when set (even empty). This is not the
    on-disk `.zarray` content: a conforming `.zarray` must exclude
    `attributes` (they live in the sibling `.zattrs` file). Use
    `to_key_value` to produce the spec-conforming split for storage.
    """
    # to_json output shares no mutable state with the model: every value
    # that can hold a mutable container is deep-copied.
    out: ZarrV2ArrayMetadataJSON = {
        "zarr_format": self.zarr_format,
        "shape": self.shape,
        "dtype": self.dtype,
        "order": self.order,
        "chunks": self.chunks,
        "fill_value": copy.deepcopy(self.fill_value),
        "dimension_separator": self.dimension_separator,
        "compressor": copy.deepcopy(self.compressor),
        "filters": copy.deepcopy(self.filters),
    }
    if self.attributes is not UNSET:
        out["attributes"] = copy.deepcopy(self.attributes)
    return out

to_key_value

to_key_value(
    *, indent: int | str | None = None
) -> Mapping[
    ZarrV2ArrayMetadataStoreKey | ZarrV2AttributesStoreKey,
    bytes,
]
Source code in src/zarr_metadata/model/_array.py
def to_key_value(
    self, *, indent: int | str | None = None
) -> Mapping[ZarrV2ArrayMetadataStoreKey | ZarrV2AttributesStoreKey, bytes]:
    # Attributes live only in the sibling `.zattrs` file; the `.zarray`
    # document must exclude them. The `.zattrs` key is present exactly
    # when attributes are set (even empty) — UNSET emits no file.
    zarray = {k: v for k, v in self.to_json().items() if k != "attributes"}
    out: dict[ZarrV2ArrayMetadataStoreKey | ZarrV2AttributesStoreKey, bytes] = {
        ZARR_V2_ARRAY_METADATA_STORE_KEY: dump_store_json(zarray, indent=indent)
    }
    if self.attributes is not UNSET:
        out[ZARR_V2_ATTRIBUTES_STORE_KEY] = dump_store_json(self.attributes, indent=indent)
    return out

update

update(
    **kwargs: Unpack[ZarrV2ArrayMetadataPartial],
) -> ZarrV2ArrayMetadata

Return a new ZarrV2ArrayMetadata with the given fields updated.

Only the constructor-settable fields listed in ZarrV2ArrayMetadataPartial can be updated; the fixed zarr_format is rejected at the type level. Each given field fully replaces its previous value.

Source code in src/zarr_metadata/model/_array.py
def update(self, **kwargs: Unpack[ZarrV2ArrayMetadataPartial]) -> ZarrV2ArrayMetadata:
    """
    Return a new `ZarrV2ArrayMetadata` with the given fields updated.

    Only the constructor-settable fields listed in
    `ZarrV2ArrayMetadataPartial` can be updated; the fixed `zarr_format` is
    rejected at the type level. Each given field fully replaces its previous
    value.
    """
    return dataclasses.replace(self, **kwargs)

ZarrV2ArrayMetadataPartial

Bases: TypedDict

Partial form of the constructor-settable fields of ZarrV2ArrayMetadata.

Every key is optional and typed with the model's own value types, so it describes valid keyword arguments to ZarrV2ArrayMetadata.update and create_default. The init=False field zarr_format is intentionally excluded, since it cannot be passed to dataclasses.replace.

Drift between this type and the model's settable fields is prevented by tests/model/test_array.py::test_v2_partial_keys_match_settable_model_fields.

Source code in src/zarr_metadata/model/_array.py
class ZarrV2ArrayMetadataPartial(TypedDict, total=False):
    """
    Partial form of the constructor-settable fields of `ZarrV2ArrayMetadata`.

    Every key is optional and typed with the model's own value types, so it
    describes valid keyword arguments to `ZarrV2ArrayMetadata.update` and
    `create_default`. The `init=False` field `zarr_format` is intentionally
    excluded, since it cannot be passed to `dataclasses.replace`.

    Drift between this type and the model's settable fields is prevented by
    `tests/model/test_array.py::test_v2_partial_keys_match_settable_model_fields`.
    """

    shape: tuple[int, ...]
    dtype: ZarrV2DataTypeMetadata
    chunks: tuple[int, ...]
    fill_value: JSONValue
    order: ZarrV2ArrayOrder
    compressor: ZarrV2CodecMetadata | None
    filters: tuple[ZarrV2CodecMetadata, ...] | None
    dimension_separator: ZarrV2ArrayDimensionSeparator
    attributes: dict[str, JSONValue] | UNSET

attributes instance-attribute

attributes: dict[str, JSONValue] | UNSET

chunks instance-attribute

chunks: tuple[int, ...]

compressor instance-attribute

compressor: ZarrV2CodecMetadata | None

dimension_separator instance-attribute

dimension_separator: ZarrV2ArrayDimensionSeparator

dtype instance-attribute

fill_value instance-attribute

fill_value: JSONValue

filters instance-attribute

filters: tuple[ZarrV2CodecMetadata, ...] | None

order instance-attribute

shape instance-attribute

shape: tuple[int, ...]

ZarrV2ConsolidatedMetadata dataclass

In-memory model of a v2 .zmetadata document.

The metadata map holds the flat file-keyed entries ("path/.zarray", "path/.zattrs", ...) verbatim, preserving the normalized JSON tree. Entries are deliberately NOT merged into per-node models: which nodes had a .zattrs file at all is information the canonical representation must keep. Interpreting entries into node models is consumer work.

Source code in src/zarr_metadata/model/_group.py
@dataclass(frozen=True, slots=True, kw_only=True)
class ZarrV2ConsolidatedMetadata:
    """In-memory model of a v2 `.zmetadata` document.

    The `metadata` map holds the flat file-keyed entries (`"path/.zarray"`,
    `"path/.zattrs"`, ...) verbatim, preserving the normalized JSON tree.
    Entries are deliberately NOT merged into per-node models: which nodes had
    a `.zattrs` file at all is information the canonical representation must
    keep. Interpreting entries into node models is consumer work.
    """

    zarr_consolidated_format: Literal[1] = field(default=1, init=False)
    metadata: dict[str, JSONValue]

    def to_json(self) -> dict[str, JSONValue]:
        # to_json output shares no mutable state with the model.
        return {
            "zarr_consolidated_format": self.zarr_consolidated_format,
            "metadata": copy.deepcopy(self.metadata),
        }

    @classmethod
    def from_json(cls, data: object) -> ZarrV2ConsolidatedMetadata:
        normalized = arrays_to_tuples(data)
        if not isinstance(normalized, Mapping):
            raise MetadataValidationError(
                [ValidationProblem((), "expected a mapping", "invalid_type")]
            )
        doc = cast("Mapping[str, object]", normalized)
        problems: list[ValidationProblem] = [
            ValidationProblem((key,), "missing required key", "missing_key")
            for key in ("zarr_consolidated_format", "metadata")
            if key not in doc
        ]
        problems.extend(
            ValidationProblem((key,), "unexpected document member", "invalid_value")
            for key in doc.keys() - {"zarr_consolidated_format", "metadata"}
        )
        if "zarr_consolidated_format" in doc and (
            not isinstance(doc["zarr_consolidated_format"], int)
            or isinstance(doc["zarr_consolidated_format"], bool)
            or doc["zarr_consolidated_format"] != 1
        ):
            problems.append(
                ValidationProblem(
                    ("zarr_consolidated_format",),
                    f"expected 1, got {doc['zarr_consolidated_format']!r}",
                    "invalid_value",
                )
            )
        if "metadata" in doc:
            entries = doc["metadata"]
            if not isinstance(entries, Mapping) or not all(
                isinstance(k, str) for k in cast("Mapping[object, object]", entries)
            ):
                problems.append(
                    ValidationProblem(
                        ("metadata",), "expected a mapping with string keys", "invalid_type"
                    )
                )
            else:
                for key, value in cast("Mapping[str, object]", entries).items():
                    problems.extend(
                        ValidationProblem(
                            ("metadata", key, *problem.loc), problem.message, problem.kind
                        )
                        for problem in validate_json(value)
                    )
        if problems:
            raise MetadataValidationError(problems)
        entries_tupled = cast(
            "dict[str, JSONValue]",
            arrays_to_tuples(dict(cast("Mapping[str, object]", doc["metadata"]))),
        )
        return cls(metadata=entries_tupled)

    @classmethod
    def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV2ConsolidatedMetadata:
        return cls.from_json(load_store_json(mapping, ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY))

    def to_key_value(
        self, *, indent: int | str | None = None
    ) -> Mapping[ZarrV2ConsolidatedMetadataStoreKey, bytes]:
        return {
            ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY: dump_store_json(self.to_json(), indent=indent)
        }

metadata instance-attribute

metadata: dict[str, JSONValue]

zarr_consolidated_format class-attribute instance-attribute

zarr_consolidated_format: Literal[1] = field(
    default=1, init=False
)

__init__

__init__(*, metadata: dict[str, JSONValue]) -> None

from_json classmethod

from_json(data: object) -> ZarrV2ConsolidatedMetadata
Source code in src/zarr_metadata/model/_group.py
@classmethod
def from_json(cls, data: object) -> ZarrV2ConsolidatedMetadata:
    normalized = arrays_to_tuples(data)
    if not isinstance(normalized, Mapping):
        raise MetadataValidationError(
            [ValidationProblem((), "expected a mapping", "invalid_type")]
        )
    doc = cast("Mapping[str, object]", normalized)
    problems: list[ValidationProblem] = [
        ValidationProblem((key,), "missing required key", "missing_key")
        for key in ("zarr_consolidated_format", "metadata")
        if key not in doc
    ]
    problems.extend(
        ValidationProblem((key,), "unexpected document member", "invalid_value")
        for key in doc.keys() - {"zarr_consolidated_format", "metadata"}
    )
    if "zarr_consolidated_format" in doc and (
        not isinstance(doc["zarr_consolidated_format"], int)
        or isinstance(doc["zarr_consolidated_format"], bool)
        or doc["zarr_consolidated_format"] != 1
    ):
        problems.append(
            ValidationProblem(
                ("zarr_consolidated_format",),
                f"expected 1, got {doc['zarr_consolidated_format']!r}",
                "invalid_value",
            )
        )
    if "metadata" in doc:
        entries = doc["metadata"]
        if not isinstance(entries, Mapping) or not all(
            isinstance(k, str) for k in cast("Mapping[object, object]", entries)
        ):
            problems.append(
                ValidationProblem(
                    ("metadata",), "expected a mapping with string keys", "invalid_type"
                )
            )
        else:
            for key, value in cast("Mapping[str, object]", entries).items():
                problems.extend(
                    ValidationProblem(
                        ("metadata", key, *problem.loc), problem.message, problem.kind
                    )
                    for problem in validate_json(value)
                )
    if problems:
        raise MetadataValidationError(problems)
    entries_tupled = cast(
        "dict[str, JSONValue]",
        arrays_to_tuples(dict(cast("Mapping[str, object]", doc["metadata"]))),
    )
    return cls(metadata=entries_tupled)

from_key_value classmethod

from_key_value(
    mapping: Mapping[str, bytes],
) -> ZarrV2ConsolidatedMetadata
Source code in src/zarr_metadata/model/_group.py
@classmethod
def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV2ConsolidatedMetadata:
    return cls.from_json(load_store_json(mapping, ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY))

to_json

to_json() -> dict[str, JSONValue]
Source code in src/zarr_metadata/model/_group.py
def to_json(self) -> dict[str, JSONValue]:
    # to_json output shares no mutable state with the model.
    return {
        "zarr_consolidated_format": self.zarr_consolidated_format,
        "metadata": copy.deepcopy(self.metadata),
    }

to_key_value

to_key_value(
    *, indent: int | str | None = None
) -> Mapping[ZarrV2ConsolidatedMetadataStoreKey, bytes]
Source code in src/zarr_metadata/model/_group.py
def to_key_value(
    self, *, indent: int | str | None = None
) -> Mapping[ZarrV2ConsolidatedMetadataStoreKey, bytes]:
    return {
        ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY: dump_store_json(self.to_json(), indent=indent)
    }

ZarrV2GroupMetadata dataclass

In-memory model of a v2 group metadata document.

A canonical, lossless representation of the .zgroup content plus the sibling .zattrs attributes, folded into a single in-memory value (mirroring the merged ZarrV2GroupMetadataJSON document form). attributes is UNSET when no .zattrs file (or merged attributes key) exists — distinct from an explicit empty .zattrs, which is {} and round-trips as a file.

Source code in src/zarr_metadata/model/_group.py
@dataclass(frozen=True, slots=True, kw_only=True)
class ZarrV2GroupMetadata:
    """In-memory model of a v2 group metadata document.

    A canonical, lossless representation of the `.zgroup` content plus the
    sibling `.zattrs` attributes, folded into a single in-memory value
    (mirroring the merged `ZarrV2GroupMetadataJSON` document form). `attributes` is
    `UNSET` when no `.zattrs` file (or merged `attributes` key) exists —
    distinct from an explicit empty `.zattrs`, which is `{}` and round-trips
    as a file.
    """

    zarr_format: Literal[2] = field(default=2, init=False)
    attributes: dict[str, JSONValue] | UNSET

    @classmethod
    def create_default(cls, **overrides: Unpack[ZarrV2GroupMetadataPartial]) -> ZarrV2GroupMetadata:
        """
        Create a default (empty) v2 group metadata model, with optional overrides.

        The default is a structurally-valid group with no attributes — the group
        analog of `list()` returning `[]`. Any field can be overridden by keyword
        (the same fields accepted by `update`).
        """
        default = cls(attributes=UNSET)
        return default.update(**overrides)

    def update(self, **kwargs: Unpack[ZarrV2GroupMetadataPartial]) -> ZarrV2GroupMetadata:
        """
        Return a new `ZarrV2GroupMetadata` with the given fields updated.

        Only the constructor-settable fields listed in
        `ZarrV2GroupMetadataPartial` can be updated; the fixed `zarr_format`
        is rejected at the type level. Each given field fully replaces its
        previous value.
        """
        return dataclasses.replace(self, **kwargs)

    def to_json(self) -> ZarrV2GroupMetadataJSON:
        """Return the merged in-memory document form.

        `attributes` is included when set (even empty). This is not the
        on-disk `.zgroup` content: a conforming `.zgroup` must exclude
        `attributes` (they live in the sibling `.zattrs` file). Use
        `to_key_value` to produce the spec-conforming split for storage.
        """
        # to_json output shares no mutable state with the model.
        out: ZarrV2GroupMetadataJSON = {"zarr_format": self.zarr_format}
        if self.attributes is not UNSET:
            out["attributes"] = copy.deepcopy(self.attributes)
        return out

    @classmethod
    def from_json(cls, data: object) -> ZarrV2GroupMetadata:
        parsed = parse_group_metadata_v2(arrays_to_tuples(data))
        return cls(attributes=(dict(parsed["attributes"]) if "attributes" in parsed else UNSET))

    @classmethod
    def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV2GroupMetadata:
        zgroup_raw = cast("object", load_store_json(mapping, ZARR_V2_GROUP_METADATA_STORE_KEY))
        if not isinstance(zgroup_raw, Mapping):
            return cls.from_json(zgroup_raw)
        zgroup = cast("Mapping[str, object]", zgroup_raw)
        if "attributes" in zgroup:
            raise MetadataValidationError(
                [
                    ValidationProblem(
                        ("attributes",),
                        "unexpected document member",
                        "invalid_value",
                    )
                ]
            )
        if ZARR_V2_ATTRIBUTES_STORE_KEY in mapping:
            zattrs = cast("object", load_store_json(mapping, ZARR_V2_ATTRIBUTES_STORE_KEY))
            return cls.from_json({**zgroup, "attributes": zattrs})
        return cls.from_json(zgroup)

    def to_key_value(
        self, *, indent: int | str | None = None
    ) -> Mapping[ZarrV2GroupMetadataStoreKey | ZarrV2AttributesStoreKey, bytes]:
        # Attributes live only in the sibling `.zattrs` file; the `.zgroup`
        # document must exclude them. The `.zattrs` key is present exactly
        # when attributes are set (even empty) — UNSET emits no file.
        zgroup = {k: v for k, v in self.to_json().items() if k != "attributes"}
        out: dict[ZarrV2GroupMetadataStoreKey | ZarrV2AttributesStoreKey, bytes] = {
            ZARR_V2_GROUP_METADATA_STORE_KEY: dump_store_json(zgroup, indent=indent)
        }
        if self.attributes is not UNSET:
            out[ZARR_V2_ATTRIBUTES_STORE_KEY] = dump_store_json(self.attributes, indent=indent)
        return out

attributes instance-attribute

attributes: dict[str, JSONValue] | UNSET

zarr_format class-attribute instance-attribute

zarr_format: Literal[2] = field(default=2, init=False)

__init__

__init__(
    *, attributes: dict[str, JSONValue] | UNSET
) -> None

create_default classmethod

create_default(
    **overrides: Unpack[ZarrV2GroupMetadataPartial],
) -> ZarrV2GroupMetadata

Create a default (empty) v2 group metadata model, with optional overrides.

The default is a structurally-valid group with no attributes — the group analog of list() returning []. Any field can be overridden by keyword (the same fields accepted by update).

Source code in src/zarr_metadata/model/_group.py
@classmethod
def create_default(cls, **overrides: Unpack[ZarrV2GroupMetadataPartial]) -> ZarrV2GroupMetadata:
    """
    Create a default (empty) v2 group metadata model, with optional overrides.

    The default is a structurally-valid group with no attributes — the group
    analog of `list()` returning `[]`. Any field can be overridden by keyword
    (the same fields accepted by `update`).
    """
    default = cls(attributes=UNSET)
    return default.update(**overrides)

from_json classmethod

from_json(data: object) -> ZarrV2GroupMetadata
Source code in src/zarr_metadata/model/_group.py
@classmethod
def from_json(cls, data: object) -> ZarrV2GroupMetadata:
    parsed = parse_group_metadata_v2(arrays_to_tuples(data))
    return cls(attributes=(dict(parsed["attributes"]) if "attributes" in parsed else UNSET))

from_key_value classmethod

from_key_value(
    mapping: Mapping[str, bytes],
) -> ZarrV2GroupMetadata
Source code in src/zarr_metadata/model/_group.py
@classmethod
def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV2GroupMetadata:
    zgroup_raw = cast("object", load_store_json(mapping, ZARR_V2_GROUP_METADATA_STORE_KEY))
    if not isinstance(zgroup_raw, Mapping):
        return cls.from_json(zgroup_raw)
    zgroup = cast("Mapping[str, object]", zgroup_raw)
    if "attributes" in zgroup:
        raise MetadataValidationError(
            [
                ValidationProblem(
                    ("attributes",),
                    "unexpected document member",
                    "invalid_value",
                )
            ]
        )
    if ZARR_V2_ATTRIBUTES_STORE_KEY in mapping:
        zattrs = cast("object", load_store_json(mapping, ZARR_V2_ATTRIBUTES_STORE_KEY))
        return cls.from_json({**zgroup, "attributes": zattrs})
    return cls.from_json(zgroup)

to_json

Return the merged in-memory document form.

attributes is included when set (even empty). This is not the on-disk .zgroup content: a conforming .zgroup must exclude attributes (they live in the sibling .zattrs file). Use to_key_value to produce the spec-conforming split for storage.

Source code in src/zarr_metadata/model/_group.py
def to_json(self) -> ZarrV2GroupMetadataJSON:
    """Return the merged in-memory document form.

    `attributes` is included when set (even empty). This is not the
    on-disk `.zgroup` content: a conforming `.zgroup` must exclude
    `attributes` (they live in the sibling `.zattrs` file). Use
    `to_key_value` to produce the spec-conforming split for storage.
    """
    # to_json output shares no mutable state with the model.
    out: ZarrV2GroupMetadataJSON = {"zarr_format": self.zarr_format}
    if self.attributes is not UNSET:
        out["attributes"] = copy.deepcopy(self.attributes)
    return out

to_key_value

to_key_value(
    *, indent: int | str | None = None
) -> Mapping[
    ZarrV2GroupMetadataStoreKey | ZarrV2AttributesStoreKey,
    bytes,
]
Source code in src/zarr_metadata/model/_group.py
def to_key_value(
    self, *, indent: int | str | None = None
) -> Mapping[ZarrV2GroupMetadataStoreKey | ZarrV2AttributesStoreKey, bytes]:
    # Attributes live only in the sibling `.zattrs` file; the `.zgroup`
    # document must exclude them. The `.zattrs` key is present exactly
    # when attributes are set (even empty) — UNSET emits no file.
    zgroup = {k: v for k, v in self.to_json().items() if k != "attributes"}
    out: dict[ZarrV2GroupMetadataStoreKey | ZarrV2AttributesStoreKey, bytes] = {
        ZARR_V2_GROUP_METADATA_STORE_KEY: dump_store_json(zgroup, indent=indent)
    }
    if self.attributes is not UNSET:
        out[ZARR_V2_ATTRIBUTES_STORE_KEY] = dump_store_json(self.attributes, indent=indent)
    return out

update

update(
    **kwargs: Unpack[ZarrV2GroupMetadataPartial],
) -> ZarrV2GroupMetadata

Return a new ZarrV2GroupMetadata with the given fields updated.

Only the constructor-settable fields listed in ZarrV2GroupMetadataPartial can be updated; the fixed zarr_format is rejected at the type level. Each given field fully replaces its previous value.

Source code in src/zarr_metadata/model/_group.py
def update(self, **kwargs: Unpack[ZarrV2GroupMetadataPartial]) -> ZarrV2GroupMetadata:
    """
    Return a new `ZarrV2GroupMetadata` with the given fields updated.

    Only the constructor-settable fields listed in
    `ZarrV2GroupMetadataPartial` can be updated; the fixed `zarr_format`
    is rejected at the type level. Each given field fully replaces its
    previous value.
    """
    return dataclasses.replace(self, **kwargs)

ZarrV2GroupMetadataPartial

Bases: TypedDict

Partial form of the constructor-settable fields of ZarrV2GroupMetadata.

Every key is optional and typed with the model's own value types, so it describes valid keyword arguments to ZarrV2GroupMetadata.update and create_default. The init=False field zarr_format is intentionally excluded, since it cannot be passed to dataclasses.replace.

Drift between this type and the model's settable fields is prevented by tests/model/test_group.py::test_group_partial_keys_match_settable_model_fields.

Source code in src/zarr_metadata/model/_group.py
class ZarrV2GroupMetadataPartial(TypedDict, total=False):
    """
    Partial form of the constructor-settable fields of `ZarrV2GroupMetadata`.

    Every key is optional and typed with the model's own value types, so it
    describes valid keyword arguments to `ZarrV2GroupMetadata.update` and
    `create_default`. The `init=False` field `zarr_format` is intentionally
    excluded, since it cannot be passed to `dataclasses.replace`.

    Drift between this type and the model's settable fields is prevented by
    `tests/model/test_group.py::test_group_partial_keys_match_settable_model_fields`.
    """

    attributes: dict[str, JSONValue] | UNSET

attributes instance-attribute

attributes: dict[str, JSONValue] | UNSET

ZarrV3ArrayMetadata dataclass

In-memory model of a v3 array metadata document.

A canonical, semantically lossless representation of the zarr.json content for an array. Extension points (data_type, chunk_grid, chunk_key_encoding, codecs, storage_transformers) are held as ZarrV3MetadataField values (currently ZarrV3NamedConfig name, configuration, and obligation records) and are never interpreted; fill_value is held verbatim in its JSON form. Equivalent extension spellings normalize to shorthand strings when configuration is empty and understanding is required.

Source code in src/zarr_metadata/model/_array.py
@dataclass(frozen=True, slots=True, kw_only=True)
class ZarrV3ArrayMetadata:
    """In-memory model of a v3 array metadata document.

    A canonical, semantically lossless representation of the `zarr.json`
    content for an array. Extension points (`data_type`, `chunk_grid`,
    `chunk_key_encoding`, `codecs`, `storage_transformers`) are held as
    `ZarrV3MetadataField` values (currently `ZarrV3NamedConfig` name,
    configuration, and obligation records) and are never interpreted;
    `fill_value` is held verbatim in its JSON form. Equivalent extension
    spellings normalize to shorthand strings when configuration is empty and
    understanding is required.
    """

    zarr_format: Literal[3] = field(default=3, init=False)
    node_type: Literal["array"] = field(default="array", init=False)
    shape: tuple[int, ...]
    fill_value: JSONValue
    data_type: ZarrV3MetadataField
    chunk_grid: ZarrV3MetadataField
    codecs: tuple[ZarrV3MetadataField, ...]
    chunk_key_encoding: ZarrV3MetadataField
    dimension_names: tuple[str | None, ...] | UNSET
    attributes: dict[str, JSONValue]
    storage_transformers: tuple[ZarrV3MetadataField, ...]
    extra_fields: dict[str, ZarrV3ExtensionField]

    @classmethod
    def create_default(cls, **overrides: Unpack[ZarrV3ArrayMetadataPartial]) -> ZarrV3ArrayMetadata:
        """
        Create a default (empty) v3 array metadata model, with optional overrides.

        The default is a structurally-valid scalar `uint8` array — the array
        analog of `list()` returning `[]`. Any field can be overridden by keyword
        (the same fields accepted by `update`). Overriding `shape` without
        `chunk_grid` derives a consistent default grid: one regular chunk
        covering the array (`chunk_shape` equal to `shape`).

        The derivation is deliberately one-way. A user-supplied `chunk_grid`
        is an extension point and is taken verbatim — deriving `shape` from
        it would require interpreting the grid's configuration, which this
        layer never does (and cannot do for unrecognized grid names). So
        overriding `chunk_grid` without `shape` keeps the scalar default
        `shape=()`, and consistency between the two is the caller's
        responsibility.
        """
        if "shape" in overrides and "chunk_grid" not in overrides:
            overrides["chunk_grid"] = ZarrV3NamedConfig(
                name="regular", configuration={"chunk_shape": tuple(overrides["shape"])}
            )
        default = cls(
            shape=(),
            fill_value=0,
            data_type=ZarrV3NamedConfig(name="uint8", configuration={}),
            chunk_grid=ZarrV3NamedConfig(name="regular", configuration={"chunk_shape": ()}),
            codecs=(ZarrV3NamedConfig(name="bytes", configuration={}),),
            chunk_key_encoding=ZarrV3NamedConfig(name="default", configuration={}),
            dimension_names=UNSET,
            attributes={},
            storage_transformers=(),
            extra_fields={},
        )
        return default.update(**overrides)

    def update(self, **kwargs: Unpack[ZarrV3ArrayMetadataPartial]) -> ZarrV3ArrayMetadata:
        """
        Return a new `ZarrV3ArrayMetadata` with the given fields updated.

        Only the constructor-settable fields listed in
        `ZarrV3ArrayMetadataPartial` can be updated; any attempt to update
        other fields (including the fixed `zarr_format` / `node_type`) is
        rejected at the type level. Each given field fully replaces its
        previous value, including `extra_fields`.

        This is useful for test fixtures that want to override a few fields of a
        base template without having to re-specify the entire document.

        No re-validation is performed (`update` is `dataclasses.replace`), so
        a repair or edit can produce an invalid document; validity is checked
        on `from_json`, not on field replacement.
        """
        return dataclasses.replace(self, **kwargs)

    def __post_init__(self) -> None:
        overlap = set(self.extra_fields.keys()).intersection(ARRAY_METADATA_STANDARD_KEYS_V3)
        if overlap:
            raise MetadataValidationError(
                [
                    ValidationProblem(
                        ("extra_fields",),
                        "Extra fields cannot overlap with standard Zarr V3 array metadata fields",
                        "invalid_value",
                    )
                ]
            )

    def to_json(self) -> ZarrV3ArrayMetadataJSON:
        # to_json output shares no mutable state with the model: every value
        # that can hold a mutable container is deep-copied.
        out: ZarrV3ArrayMetadataJSON = {
            "zarr_format": self.zarr_format,
            "node_type": self.node_type,
            "shape": self.shape,
            "fill_value": copy.deepcopy(self.fill_value),
            "data_type": self.data_type.to_json(),
            "chunk_grid": self.chunk_grid.to_json(),
            "codecs": tuple(codec.to_json() for codec in self.codecs),
            "chunk_key_encoding": self.chunk_key_encoding.to_json(),
        }
        if self.dimension_names is not UNSET:
            out["dimension_names"] = self.dimension_names
        if len(self.attributes) > 0:
            out["attributes"] = copy.deepcopy(self.attributes)
        if len(self.storage_transformers) > 0:
            out["storage_transformers"] = tuple(
                transformer.to_json() for transformer in self.storage_transformers
            )
        # Extra fields are the TypedDict's `extra_items` (PEP 728). Assign them
        # by key rather than `out.update(**...)`: type checkers understand the
        # indexed-write path against `extra_items`, but not the `update(**...)`
        # overload.
        for key, value in self.extra_fields.items():
            out[key] = copy.deepcopy(value)
        return out

    @classmethod
    def from_json(cls, data: object) -> ZarrV3ArrayMetadata:
        parsed = parse_array_metadata_v3(arrays_to_tuples(data))
        # Sound cast: the TypedDict types all non-standard keys as its
        # `extra_items` (`ZarrV3ExtensionField`); the comprehension's inferred value
        # type is the union over ALL keys because the key filter cannot narrow it.
        extra_fields = cast(
            "dict[str, ZarrV3ExtensionField]",
            {k: v for k, v in parsed.items() if k not in ARRAY_METADATA_STANDARD_KEYS_V3},
        )
        return cls(
            shape=parsed["shape"],
            fill_value=parsed["fill_value"],
            data_type=ZarrV3NamedConfig.from_json(parsed["data_type"]),
            chunk_grid=ZarrV3NamedConfig.from_json(parsed["chunk_grid"]),
            codecs=tuple(ZarrV3NamedConfig.from_json(c) for c in parsed["codecs"]),
            chunk_key_encoding=ZarrV3NamedConfig.from_json(parsed["chunk_key_encoding"]),
            dimension_names=parsed.get("dimension_names", UNSET),
            attributes=dict(parsed.get("attributes", {})),
            storage_transformers=tuple(
                ZarrV3NamedConfig.from_json(t) for t in parsed.get("storage_transformers", ())
            ),
            extra_fields=extra_fields,
        )

    @property
    def must_understand_fields(self) -> dict[str, ZarrV3ExtensionField]:
        """Extra fields the reader is obligated to understand.

        Everything in `extra_fields` not explicitly waived with
        `must_understand: false` (the spec's implicit-true rule). A compliant
        reader MUST fail to open the array if this contains any field it does
        not recognize; the model layer only partitions by obligation, since
        recognition is reader-specific.
        """
        return must_understand_subset(self.extra_fields)

    @classmethod
    def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV3ArrayMetadata:
        return cls.from_json(load_store_json(mapping, ZARR_V3_ARRAY_METADATA_STORE_KEY))

    def to_key_value(
        self, *, indent: int | str | None = None
    ) -> Mapping[ZarrV3ArrayMetadataStoreKey, bytes]:
        return {ZARR_V3_ARRAY_METADATA_STORE_KEY: dump_store_json(self.to_json(), indent=indent)}

attributes instance-attribute

attributes: dict[str, JSONValue]

chunk_grid instance-attribute

chunk_grid: ZarrV3MetadataField

chunk_key_encoding instance-attribute

chunk_key_encoding: ZarrV3MetadataField

codecs instance-attribute

codecs: tuple[ZarrV3MetadataField, ...]

data_type instance-attribute

dimension_names instance-attribute

dimension_names: tuple[str | None, ...] | UNSET

extra_fields instance-attribute

extra_fields: dict[str, ZarrV3ExtensionField]

fill_value instance-attribute

fill_value: JSONValue

must_understand_fields property

must_understand_fields: dict[str, ZarrV3ExtensionField]

Extra fields the reader is obligated to understand.

Everything in extra_fields not explicitly waived with must_understand: false (the spec's implicit-true rule). A compliant reader MUST fail to open the array if this contains any field it does not recognize; the model layer only partitions by obligation, since recognition is reader-specific.

node_type class-attribute instance-attribute

node_type: Literal["array"] = field(
    default="array", init=False
)

shape instance-attribute

shape: tuple[int, ...]

storage_transformers instance-attribute

storage_transformers: tuple[ZarrV3MetadataField, ...]

zarr_format class-attribute instance-attribute

zarr_format: Literal[3] = field(default=3, init=False)

__init__

__init__(
    *,
    shape: tuple[int, ...],
    fill_value: JSONValue,
    data_type: ZarrV3MetadataField,
    chunk_grid: ZarrV3MetadataField,
    codecs: tuple[ZarrV3MetadataField, ...],
    chunk_key_encoding: ZarrV3MetadataField,
    dimension_names: tuple[str | None, ...] | UNSET,
    attributes: dict[str, JSONValue],
    storage_transformers: tuple[ZarrV3MetadataField, ...],
    extra_fields: dict[str, ZarrV3ExtensionField],
) -> None

__post_init__

__post_init__() -> None
Source code in src/zarr_metadata/model/_array.py
def __post_init__(self) -> None:
    overlap = set(self.extra_fields.keys()).intersection(ARRAY_METADATA_STANDARD_KEYS_V3)
    if overlap:
        raise MetadataValidationError(
            [
                ValidationProblem(
                    ("extra_fields",),
                    "Extra fields cannot overlap with standard Zarr V3 array metadata fields",
                    "invalid_value",
                )
            ]
        )

create_default classmethod

create_default(
    **overrides: Unpack[ZarrV3ArrayMetadataPartial],
) -> ZarrV3ArrayMetadata

Create a default (empty) v3 array metadata model, with optional overrides.

The default is a structurally-valid scalar uint8 array — the array analog of list() returning []. Any field can be overridden by keyword (the same fields accepted by update). Overriding shape without chunk_grid derives a consistent default grid: one regular chunk covering the array (chunk_shape equal to shape).

The derivation is deliberately one-way. A user-supplied chunk_grid is an extension point and is taken verbatim — deriving shape from it would require interpreting the grid's configuration, which this layer never does (and cannot do for unrecognized grid names). So overriding chunk_grid without shape keeps the scalar default shape=(), and consistency between the two is the caller's responsibility.

Source code in src/zarr_metadata/model/_array.py
@classmethod
def create_default(cls, **overrides: Unpack[ZarrV3ArrayMetadataPartial]) -> ZarrV3ArrayMetadata:
    """
    Create a default (empty) v3 array metadata model, with optional overrides.

    The default is a structurally-valid scalar `uint8` array — the array
    analog of `list()` returning `[]`. Any field can be overridden by keyword
    (the same fields accepted by `update`). Overriding `shape` without
    `chunk_grid` derives a consistent default grid: one regular chunk
    covering the array (`chunk_shape` equal to `shape`).

    The derivation is deliberately one-way. A user-supplied `chunk_grid`
    is an extension point and is taken verbatim — deriving `shape` from
    it would require interpreting the grid's configuration, which this
    layer never does (and cannot do for unrecognized grid names). So
    overriding `chunk_grid` without `shape` keeps the scalar default
    `shape=()`, and consistency between the two is the caller's
    responsibility.
    """
    if "shape" in overrides and "chunk_grid" not in overrides:
        overrides["chunk_grid"] = ZarrV3NamedConfig(
            name="regular", configuration={"chunk_shape": tuple(overrides["shape"])}
        )
    default = cls(
        shape=(),
        fill_value=0,
        data_type=ZarrV3NamedConfig(name="uint8", configuration={}),
        chunk_grid=ZarrV3NamedConfig(name="regular", configuration={"chunk_shape": ()}),
        codecs=(ZarrV3NamedConfig(name="bytes", configuration={}),),
        chunk_key_encoding=ZarrV3NamedConfig(name="default", configuration={}),
        dimension_names=UNSET,
        attributes={},
        storage_transformers=(),
        extra_fields={},
    )
    return default.update(**overrides)

from_json classmethod

from_json(data: object) -> ZarrV3ArrayMetadata
Source code in src/zarr_metadata/model/_array.py
@classmethod
def from_json(cls, data: object) -> ZarrV3ArrayMetadata:
    parsed = parse_array_metadata_v3(arrays_to_tuples(data))
    # Sound cast: the TypedDict types all non-standard keys as its
    # `extra_items` (`ZarrV3ExtensionField`); the comprehension's inferred value
    # type is the union over ALL keys because the key filter cannot narrow it.
    extra_fields = cast(
        "dict[str, ZarrV3ExtensionField]",
        {k: v for k, v in parsed.items() if k not in ARRAY_METADATA_STANDARD_KEYS_V3},
    )
    return cls(
        shape=parsed["shape"],
        fill_value=parsed["fill_value"],
        data_type=ZarrV3NamedConfig.from_json(parsed["data_type"]),
        chunk_grid=ZarrV3NamedConfig.from_json(parsed["chunk_grid"]),
        codecs=tuple(ZarrV3NamedConfig.from_json(c) for c in parsed["codecs"]),
        chunk_key_encoding=ZarrV3NamedConfig.from_json(parsed["chunk_key_encoding"]),
        dimension_names=parsed.get("dimension_names", UNSET),
        attributes=dict(parsed.get("attributes", {})),
        storage_transformers=tuple(
            ZarrV3NamedConfig.from_json(t) for t in parsed.get("storage_transformers", ())
        ),
        extra_fields=extra_fields,
    )

from_key_value classmethod

from_key_value(
    mapping: Mapping[str, bytes],
) -> ZarrV3ArrayMetadata
Source code in src/zarr_metadata/model/_array.py
@classmethod
def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV3ArrayMetadata:
    return cls.from_json(load_store_json(mapping, ZARR_V3_ARRAY_METADATA_STORE_KEY))

to_json

Source code in src/zarr_metadata/model/_array.py
def to_json(self) -> ZarrV3ArrayMetadataJSON:
    # to_json output shares no mutable state with the model: every value
    # that can hold a mutable container is deep-copied.
    out: ZarrV3ArrayMetadataJSON = {
        "zarr_format": self.zarr_format,
        "node_type": self.node_type,
        "shape": self.shape,
        "fill_value": copy.deepcopy(self.fill_value),
        "data_type": self.data_type.to_json(),
        "chunk_grid": self.chunk_grid.to_json(),
        "codecs": tuple(codec.to_json() for codec in self.codecs),
        "chunk_key_encoding": self.chunk_key_encoding.to_json(),
    }
    if self.dimension_names is not UNSET:
        out["dimension_names"] = self.dimension_names
    if len(self.attributes) > 0:
        out["attributes"] = copy.deepcopy(self.attributes)
    if len(self.storage_transformers) > 0:
        out["storage_transformers"] = tuple(
            transformer.to_json() for transformer in self.storage_transformers
        )
    # Extra fields are the TypedDict's `extra_items` (PEP 728). Assign them
    # by key rather than `out.update(**...)`: type checkers understand the
    # indexed-write path against `extra_items`, but not the `update(**...)`
    # overload.
    for key, value in self.extra_fields.items():
        out[key] = copy.deepcopy(value)
    return out

to_key_value

to_key_value(
    *, indent: int | str | None = None
) -> Mapping[ZarrV3ArrayMetadataStoreKey, bytes]
Source code in src/zarr_metadata/model/_array.py
def to_key_value(
    self, *, indent: int | str | None = None
) -> Mapping[ZarrV3ArrayMetadataStoreKey, bytes]:
    return {ZARR_V3_ARRAY_METADATA_STORE_KEY: dump_store_json(self.to_json(), indent=indent)}

update

update(
    **kwargs: Unpack[ZarrV3ArrayMetadataPartial],
) -> ZarrV3ArrayMetadata

Return a new ZarrV3ArrayMetadata with the given fields updated.

Only the constructor-settable fields listed in ZarrV3ArrayMetadataPartial can be updated; any attempt to update other fields (including the fixed zarr_format / node_type) is rejected at the type level. Each given field fully replaces its previous value, including extra_fields.

This is useful for test fixtures that want to override a few fields of a base template without having to re-specify the entire document.

No re-validation is performed (update is dataclasses.replace), so a repair or edit can produce an invalid document; validity is checked on from_json, not on field replacement.

Source code in src/zarr_metadata/model/_array.py
def update(self, **kwargs: Unpack[ZarrV3ArrayMetadataPartial]) -> ZarrV3ArrayMetadata:
    """
    Return a new `ZarrV3ArrayMetadata` with the given fields updated.

    Only the constructor-settable fields listed in
    `ZarrV3ArrayMetadataPartial` can be updated; any attempt to update
    other fields (including the fixed `zarr_format` / `node_type`) is
    rejected at the type level. Each given field fully replaces its
    previous value, including `extra_fields`.

    This is useful for test fixtures that want to override a few fields of a
    base template without having to re-specify the entire document.

    No re-validation is performed (`update` is `dataclasses.replace`), so
    a repair or edit can produce an invalid document; validity is checked
    on `from_json`, not on field replacement.
    """
    return dataclasses.replace(self, **kwargs)

ZarrV3ArrayMetadataPartial

Bases: TypedDict

Partial form of the constructor-settable fields of ZarrV3ArrayMetadata.

Every key is optional and typed with the model's own (not serialized) value types, so it describes valid keyword arguments to ZarrV3ArrayMetadata.update. The init=False fields zarr_format and node_type are intentionally excluded, since they cannot be passed to dataclasses.replace.

Drift between this type and the model's settable fields is prevented by tests/model/test_array.py::test_partial_keys_match_settable_model_fields.

Source code in src/zarr_metadata/model/_array.py
class ZarrV3ArrayMetadataPartial(TypedDict, total=False):
    """
    Partial form of the constructor-settable fields of `ZarrV3ArrayMetadata`.

    Every key is optional and typed with the model's own (not serialized)
    value types, so it describes valid keyword arguments to
    `ZarrV3ArrayMetadata.update`. The `init=False` fields `zarr_format` and
    `node_type` are intentionally excluded, since they cannot be passed to
    `dataclasses.replace`.

    Drift between this type and the model's settable fields is prevented by
    `tests/model/test_array.py::test_partial_keys_match_settable_model_fields`.
    """

    shape: tuple[int, ...]
    fill_value: JSONValue
    data_type: ZarrV3MetadataField
    chunk_grid: ZarrV3MetadataField
    codecs: tuple[ZarrV3MetadataField, ...]
    chunk_key_encoding: ZarrV3MetadataField
    dimension_names: tuple[str | None, ...] | UNSET
    attributes: dict[str, JSONValue]
    storage_transformers: tuple[ZarrV3MetadataField, ...]
    extra_fields: dict[str, ZarrV3ExtensionField]

attributes instance-attribute

attributes: dict[str, JSONValue]

chunk_grid instance-attribute

chunk_grid: ZarrV3MetadataField

chunk_key_encoding instance-attribute

chunk_key_encoding: ZarrV3MetadataField

codecs instance-attribute

codecs: tuple[ZarrV3MetadataField, ...]

data_type instance-attribute

dimension_names instance-attribute

dimension_names: tuple[str | None, ...] | UNSET

extra_fields instance-attribute

extra_fields: dict[str, ZarrV3ExtensionField]

fill_value instance-attribute

fill_value: JSONValue

shape instance-attribute

shape: tuple[int, ...]

storage_transformers instance-attribute

storage_transformers: tuple[ZarrV3MetadataField, ...]

ZarrV3ConsolidatedMetadata dataclass

In-memory model of v3 inline consolidated metadata.

Models the reference-implementation convention where consolidated metadata is embedded as an extension field on a group's zarr.json. Each entry in metadata is a complete child document, held as a thin array or group model. must_understand is typed permissively as bool to mirror the document shape, but only False is valid; this is enforced at runtime.

Source code in src/zarr_metadata/model/_group.py
@dataclass(frozen=True, slots=True, kw_only=True)
class ZarrV3ConsolidatedMetadata:
    """In-memory model of v3 inline consolidated metadata.

    Models the reference-implementation convention where consolidated metadata
    is embedded as an extension field on a group's `zarr.json`. Each entry in
    `metadata` is a complete child document, held as a thin array or group
    model. `must_understand` is typed permissively as `bool` to mirror the
    document shape, but only `False` is valid; this is enforced at runtime.
    """

    kind: Literal["inline"] = field(default="inline", init=False)
    must_understand: bool = False
    metadata: dict[str, ZarrV3ArrayMetadata | ZarrV3GroupMetadata]

    def __post_init__(self) -> None:
        if self.must_understand is not False:
            raise MetadataValidationError(
                [
                    ValidationProblem(
                        ("must_understand",),
                        f"Invalid value for 'must_understand'. Expected False. "
                        f"Got {self.must_understand!r}.",
                        "invalid_value",
                    )
                ]
            )

    def to_json(self) -> ZarrV3ConsolidatedMetadataJSON:
        # `must_understand` is emitted as the literal False: the field is typed
        # permissively as `bool`, but `__post_init__` guarantees the value.
        return {
            "kind": self.kind,
            "must_understand": False,
            "metadata": {key: node.to_json() for key, node in self.metadata.items()},
        }

    @classmethod
    def from_json(cls, data: object) -> ZarrV3ConsolidatedMetadata:
        normalized = arrays_to_tuples(data)
        problems = validate_consolidated_metadata_v3(normalized)
        if problems:
            raise MetadataValidationError(problems)
        env = cast("Mapping[str, object]", normalized)
        entries: dict[str, ZarrV3ArrayMetadata | ZarrV3GroupMetadata] = {}
        for key, entry in cast("Mapping[str, object]", env["metadata"]).items():
            node_type = cast("Mapping[str, object]", entry).get("node_type")
            if node_type == "array":
                entries[key] = ZarrV3ArrayMetadata.from_json(entry)
            else:
                entries[key] = ZarrV3GroupMetadata.from_json(entry)
        return cls(metadata=entries)

kind class-attribute instance-attribute

kind: Literal["inline"] = field(
    default="inline", init=False
)

metadata instance-attribute

must_understand class-attribute instance-attribute

must_understand: bool = False

__init__

__init__(
    *,
    must_understand: bool = False,
    metadata: dict[
        str, ZarrV3ArrayMetadata | ZarrV3GroupMetadata
    ],
) -> None

__post_init__

__post_init__() -> None
Source code in src/zarr_metadata/model/_group.py
def __post_init__(self) -> None:
    if self.must_understand is not False:
        raise MetadataValidationError(
            [
                ValidationProblem(
                    ("must_understand",),
                    f"Invalid value for 'must_understand'. Expected False. "
                    f"Got {self.must_understand!r}.",
                    "invalid_value",
                )
            ]
        )

from_json classmethod

from_json(data: object) -> ZarrV3ConsolidatedMetadata
Source code in src/zarr_metadata/model/_group.py
@classmethod
def from_json(cls, data: object) -> ZarrV3ConsolidatedMetadata:
    normalized = arrays_to_tuples(data)
    problems = validate_consolidated_metadata_v3(normalized)
    if problems:
        raise MetadataValidationError(problems)
    env = cast("Mapping[str, object]", normalized)
    entries: dict[str, ZarrV3ArrayMetadata | ZarrV3GroupMetadata] = {}
    for key, entry in cast("Mapping[str, object]", env["metadata"]).items():
        node_type = cast("Mapping[str, object]", entry).get("node_type")
        if node_type == "array":
            entries[key] = ZarrV3ArrayMetadata.from_json(entry)
        else:
            entries[key] = ZarrV3GroupMetadata.from_json(entry)
    return cls(metadata=entries)

to_json

Source code in src/zarr_metadata/model/_group.py
def to_json(self) -> ZarrV3ConsolidatedMetadataJSON:
    # `must_understand` is emitted as the literal False: the field is typed
    # permissively as `bool`, but `__post_init__` guarantees the value.
    return {
        "kind": self.kind,
        "must_understand": False,
        "metadata": {key: node.to_json() for key, node in self.metadata.items()},
    }

ZarrV3GroupMetadata dataclass

In-memory model of a v3 group metadata document.

A canonical, semantically lossless representation of the zarr.json content for a group. The consolidated_metadata reference-implementation convention is modeled as a typed field holding thin child models; every other unknown top-level key lands in extra_fields verbatim.

Source code in src/zarr_metadata/model/_group.py
@dataclass(frozen=True, slots=True, kw_only=True)
class ZarrV3GroupMetadata:
    """In-memory model of a v3 group metadata document.

    A canonical, semantically lossless representation of the `zarr.json`
    content for a group. The `consolidated_metadata` reference-implementation
    convention is modeled as a typed field holding thin child models; every
    other unknown top-level key lands in `extra_fields` verbatim.
    """

    zarr_format: Literal[3] = field(default=3, init=False)
    node_type: Literal["group"] = field(default="group", init=False)
    attributes: dict[str, JSONValue]
    consolidated_metadata: ZarrV3ConsolidatedMetadata | UNSET
    extra_fields: dict[str, ZarrV3ExtensionField]

    def __post_init__(self) -> None:
        reserved = GROUP_METADATA_STANDARD_KEYS_V3 | {ZARR_V3_CONSOLIDATED_METADATA_KEY}
        if set(self.extra_fields.keys()).intersection(reserved):
            raise MetadataValidationError(
                [
                    ValidationProblem(
                        ("extra_fields",),
                        "Extra fields cannot overlap with standard Zarr V3 group metadata fields",
                        "invalid_value",
                    )
                ]
            )

    @classmethod
    def create_default(cls, **overrides: Unpack[ZarrV3GroupMetadataPartial]) -> ZarrV3GroupMetadata:
        """
        Create a default (empty) v3 group metadata model, with optional overrides.

        The default is a structurally-valid group with no attributes — the group
        analog of `list()` returning `[]`. Any field can be overridden by keyword
        (the same fields accepted by `update`).
        """
        default = cls(attributes={}, consolidated_metadata=UNSET, extra_fields={})
        return default.update(**overrides)

    def update(self, **kwargs: Unpack[ZarrV3GroupMetadataPartial]) -> ZarrV3GroupMetadata:
        """
        Return a new `ZarrV3GroupMetadata` with the given fields updated.

        Only the constructor-settable fields listed in
        `ZarrV3GroupMetadataPartial` can be updated; the fixed `zarr_format` /
        `node_type` are rejected at the type level. Each given field fully
        replaces its previous value, including `extra_fields`.
        """
        return dataclasses.replace(self, **kwargs)

    def to_json(self) -> ZarrV3GroupMetadataJSON:
        # to_json output shares no mutable state with the model: every value
        # that can hold a mutable container is deep-copied.
        out: ZarrV3GroupMetadataJSON = {
            "zarr_format": self.zarr_format,
            "node_type": self.node_type,
        }
        if len(self.attributes) > 0:
            out["attributes"] = copy.deepcopy(self.attributes)
        if self.consolidated_metadata is not UNSET:
            # Consolidated metadata is a known non-core top-level JSON field.
            out[ZARR_V3_CONSOLIDATED_METADATA_KEY] = cast(
                "ZarrV3ExtensionField", self.consolidated_metadata.to_json()
            )
        for key, value in self.extra_fields.items():
            out[key] = copy.deepcopy(value)
        return out

    @classmethod
    def from_json(cls, data: object) -> ZarrV3GroupMetadata:
        parsed = parse_group_metadata_v3(arrays_to_tuples(data))
        # Cast for narrowing across standard and arbitrary extra TypedDict items.
        consolidated_raw = cast("object", parsed.get(ZARR_V3_CONSOLIDATED_METADATA_KEY, UNSET))
        consolidated: ZarrV3ConsolidatedMetadata | UNSET
        if consolidated_raw is UNSET or consolidated_raw is None:
            # consolidated_metadata: null was written by a historical
            # zarr-python bug; it gets no model representation. It is read as
            # absence and never written back — repaired, not preserved.
            consolidated = UNSET
        else:
            consolidated = ZarrV3ConsolidatedMetadata.from_json(consolidated_raw)
        # Sound cast: the TypedDict types all non-standard keys as its
        # `extra_items` (`ZarrV3ExtensionField`); the comprehension's inferred value
        # type is the union over ALL keys because the key filter cannot narrow it.
        extra_fields = cast(
            "dict[str, ZarrV3ExtensionField]",
            {
                k: v
                for k, v in parsed.items()
                if k not in GROUP_METADATA_STANDARD_KEYS_V3
                and k != ZARR_V3_CONSOLIDATED_METADATA_KEY
            },
        )
        return cls(
            attributes=dict(parsed.get("attributes", {})),
            consolidated_metadata=consolidated,
            extra_fields=extra_fields,
        )

    @property
    def must_understand_fields(self) -> dict[str, ZarrV3ExtensionField]:
        """Extra fields the reader is obligated to understand.

        Everything in `extra_fields` not explicitly waived with
        `must_understand: false` (the spec's implicit-true rule). A compliant
        reader MUST fail to open the group if this contains any field it does
        not recognize; the model layer only partitions by obligation, since
        recognition is reader-specific.
        """
        return must_understand_subset(self.extra_fields)

    @classmethod
    def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV3GroupMetadata:
        return cls.from_json(load_store_json(mapping, ZARR_V3_GROUP_METADATA_STORE_KEY))

    def to_key_value(
        self, *, indent: int | str | None = None
    ) -> Mapping[ZarrV3GroupMetadataStoreKey, bytes]:
        return {ZARR_V3_GROUP_METADATA_STORE_KEY: dump_store_json(self.to_json(), indent=indent)}

attributes instance-attribute

attributes: dict[str, JSONValue]

consolidated_metadata instance-attribute

consolidated_metadata: ZarrV3ConsolidatedMetadata | UNSET

extra_fields instance-attribute

extra_fields: dict[str, ZarrV3ExtensionField]

must_understand_fields property

must_understand_fields: dict[str, ZarrV3ExtensionField]

Extra fields the reader is obligated to understand.

Everything in extra_fields not explicitly waived with must_understand: false (the spec's implicit-true rule). A compliant reader MUST fail to open the group if this contains any field it does not recognize; the model layer only partitions by obligation, since recognition is reader-specific.

node_type class-attribute instance-attribute

node_type: Literal["group"] = field(
    default="group", init=False
)

zarr_format class-attribute instance-attribute

zarr_format: Literal[3] = field(default=3, init=False)

__init__

__init__(
    *,
    attributes: dict[str, JSONValue],
    consolidated_metadata: ZarrV3ConsolidatedMetadata
    | UNSET,
    extra_fields: dict[str, ZarrV3ExtensionField],
) -> None

__post_init__

__post_init__() -> None
Source code in src/zarr_metadata/model/_group.py
def __post_init__(self) -> None:
    reserved = GROUP_METADATA_STANDARD_KEYS_V3 | {ZARR_V3_CONSOLIDATED_METADATA_KEY}
    if set(self.extra_fields.keys()).intersection(reserved):
        raise MetadataValidationError(
            [
                ValidationProblem(
                    ("extra_fields",),
                    "Extra fields cannot overlap with standard Zarr V3 group metadata fields",
                    "invalid_value",
                )
            ]
        )

create_default classmethod

create_default(
    **overrides: Unpack[ZarrV3GroupMetadataPartial],
) -> ZarrV3GroupMetadata

Create a default (empty) v3 group metadata model, with optional overrides.

The default is a structurally-valid group with no attributes — the group analog of list() returning []. Any field can be overridden by keyword (the same fields accepted by update).

Source code in src/zarr_metadata/model/_group.py
@classmethod
def create_default(cls, **overrides: Unpack[ZarrV3GroupMetadataPartial]) -> ZarrV3GroupMetadata:
    """
    Create a default (empty) v3 group metadata model, with optional overrides.

    The default is a structurally-valid group with no attributes — the group
    analog of `list()` returning `[]`. Any field can be overridden by keyword
    (the same fields accepted by `update`).
    """
    default = cls(attributes={}, consolidated_metadata=UNSET, extra_fields={})
    return default.update(**overrides)

from_json classmethod

from_json(data: object) -> ZarrV3GroupMetadata
Source code in src/zarr_metadata/model/_group.py
@classmethod
def from_json(cls, data: object) -> ZarrV3GroupMetadata:
    parsed = parse_group_metadata_v3(arrays_to_tuples(data))
    # Cast for narrowing across standard and arbitrary extra TypedDict items.
    consolidated_raw = cast("object", parsed.get(ZARR_V3_CONSOLIDATED_METADATA_KEY, UNSET))
    consolidated: ZarrV3ConsolidatedMetadata | UNSET
    if consolidated_raw is UNSET or consolidated_raw is None:
        # consolidated_metadata: null was written by a historical
        # zarr-python bug; it gets no model representation. It is read as
        # absence and never written back — repaired, not preserved.
        consolidated = UNSET
    else:
        consolidated = ZarrV3ConsolidatedMetadata.from_json(consolidated_raw)
    # Sound cast: the TypedDict types all non-standard keys as its
    # `extra_items` (`ZarrV3ExtensionField`); the comprehension's inferred value
    # type is the union over ALL keys because the key filter cannot narrow it.
    extra_fields = cast(
        "dict[str, ZarrV3ExtensionField]",
        {
            k: v
            for k, v in parsed.items()
            if k not in GROUP_METADATA_STANDARD_KEYS_V3
            and k != ZARR_V3_CONSOLIDATED_METADATA_KEY
        },
    )
    return cls(
        attributes=dict(parsed.get("attributes", {})),
        consolidated_metadata=consolidated,
        extra_fields=extra_fields,
    )

from_key_value classmethod

from_key_value(
    mapping: Mapping[str, bytes],
) -> ZarrV3GroupMetadata
Source code in src/zarr_metadata/model/_group.py
@classmethod
def from_key_value(cls, mapping: Mapping[str, bytes]) -> ZarrV3GroupMetadata:
    return cls.from_json(load_store_json(mapping, ZARR_V3_GROUP_METADATA_STORE_KEY))

to_json

Source code in src/zarr_metadata/model/_group.py
def to_json(self) -> ZarrV3GroupMetadataJSON:
    # to_json output shares no mutable state with the model: every value
    # that can hold a mutable container is deep-copied.
    out: ZarrV3GroupMetadataJSON = {
        "zarr_format": self.zarr_format,
        "node_type": self.node_type,
    }
    if len(self.attributes) > 0:
        out["attributes"] = copy.deepcopy(self.attributes)
    if self.consolidated_metadata is not UNSET:
        # Consolidated metadata is a known non-core top-level JSON field.
        out[ZARR_V3_CONSOLIDATED_METADATA_KEY] = cast(
            "ZarrV3ExtensionField", self.consolidated_metadata.to_json()
        )
    for key, value in self.extra_fields.items():
        out[key] = copy.deepcopy(value)
    return out

to_key_value

to_key_value(
    *, indent: int | str | None = None
) -> Mapping[ZarrV3GroupMetadataStoreKey, bytes]
Source code in src/zarr_metadata/model/_group.py
def to_key_value(
    self, *, indent: int | str | None = None
) -> Mapping[ZarrV3GroupMetadataStoreKey, bytes]:
    return {ZARR_V3_GROUP_METADATA_STORE_KEY: dump_store_json(self.to_json(), indent=indent)}

update

update(
    **kwargs: Unpack[ZarrV3GroupMetadataPartial],
) -> ZarrV3GroupMetadata

Return a new ZarrV3GroupMetadata with the given fields updated.

Only the constructor-settable fields listed in ZarrV3GroupMetadataPartial can be updated; the fixed zarr_format / node_type are rejected at the type level. Each given field fully replaces its previous value, including extra_fields.

Source code in src/zarr_metadata/model/_group.py
def update(self, **kwargs: Unpack[ZarrV3GroupMetadataPartial]) -> ZarrV3GroupMetadata:
    """
    Return a new `ZarrV3GroupMetadata` with the given fields updated.

    Only the constructor-settable fields listed in
    `ZarrV3GroupMetadataPartial` can be updated; the fixed `zarr_format` /
    `node_type` are rejected at the type level. Each given field fully
    replaces its previous value, including `extra_fields`.
    """
    return dataclasses.replace(self, **kwargs)

ZarrV3GroupMetadataPartial

Bases: TypedDict

Partial form of the constructor-settable fields of ZarrV3GroupMetadata.

Every key is optional and typed with the model's own value types, so it describes valid keyword arguments to ZarrV3GroupMetadata.update and create_default. The init=False fields zarr_format and node_type are intentionally excluded, since they cannot be passed to dataclasses.replace.

Drift between this type and the model's settable fields is prevented by tests/model/test_group.py::test_group_partial_keys_match_settable_model_fields.

Source code in src/zarr_metadata/model/_group.py
class ZarrV3GroupMetadataPartial(TypedDict, total=False):
    """
    Partial form of the constructor-settable fields of `ZarrV3GroupMetadata`.

    Every key is optional and typed with the model's own value types, so it
    describes valid keyword arguments to `ZarrV3GroupMetadata.update` and
    `create_default`. The `init=False` fields `zarr_format` and `node_type`
    are intentionally excluded, since they cannot be passed to
    `dataclasses.replace`.

    Drift between this type and the model's settable fields is prevented by
    `tests/model/test_group.py::test_group_partial_keys_match_settable_model_fields`.
    """

    attributes: dict[str, JSONValue]
    consolidated_metadata: ZarrV3ConsolidatedMetadata | UNSET
    extra_fields: dict[str, ZarrV3ExtensionField]

attributes instance-attribute

attributes: dict[str, JSONValue]

consolidated_metadata instance-attribute

consolidated_metadata: ZarrV3ConsolidatedMetadata | UNSET

extra_fields instance-attribute

extra_fields: dict[str, ZarrV3ExtensionField]

ZarrV3NamedConfig dataclass

A normalized v3 metadata field with its reader obligation.

Bare names and missing configurations normalize to an empty configuration. Bare names and missing must_understand members normalize to the spec's implicit True value.

Source code in src/zarr_metadata/model/_array.py
@dataclass(frozen=True, slots=True, kw_only=True)
class ZarrV3NamedConfig:
    """A normalized v3 metadata field with its reader obligation.

    Bare names and missing configurations normalize to an empty configuration.
    Bare names and missing `must_understand` members normalize to the spec's
    implicit `True` value.
    """

    name: str
    configuration: dict[str, JSONValue]
    must_understand: bool = True

    def to_json(self) -> ZarrV3MetadataFieldJSON:
        if not self.configuration and self.must_understand:
            return self.name
        out: ZarrV3NamedConfigJSON = {"name": self.name}
        if self.configuration:
            # to_json output shares no mutable state with the model.
            out["configuration"] = copy.deepcopy(self.configuration)
        if not self.must_understand:
            out["must_understand"] = False
        return out

    @classmethod
    def from_json(cls, data: object) -> ZarrV3NamedConfig:
        field = parse_metadata_field_v3(data)
        if isinstance(field, str):
            return cls(name=field, configuration={}, must_understand=True)
        # Sound cast: parse_metadata_field_v3 checked the configuration is a
        # string-keyed mapping of JSON values; arrays_to_tuples only converts
        # lists to tuples within that shape.
        configuration = cast(
            "dict[str, JSONValue]", arrays_to_tuples(dict(field.get("configuration", {})))
        )
        return cls(
            name=field["name"],
            configuration=configuration,
            must_understand=field.get("must_understand", True),
        )

configuration instance-attribute

configuration: dict[str, JSONValue]

must_understand class-attribute instance-attribute

must_understand: bool = True

name instance-attribute

name: str

__init__

__init__(
    *,
    name: str,
    configuration: dict[str, JSONValue],
    must_understand: bool = True,
) -> None

from_json classmethod

from_json(data: object) -> ZarrV3NamedConfig
Source code in src/zarr_metadata/model/_array.py
@classmethod
def from_json(cls, data: object) -> ZarrV3NamedConfig:
    field = parse_metadata_field_v3(data)
    if isinstance(field, str):
        return cls(name=field, configuration={}, must_understand=True)
    # Sound cast: parse_metadata_field_v3 checked the configuration is a
    # string-keyed mapping of JSON values; arrays_to_tuples only converts
    # lists to tuples within that shape.
    configuration = cast(
        "dict[str, JSONValue]", arrays_to_tuples(dict(field.get("configuration", {})))
    )
    return cls(
        name=field["name"],
        configuration=configuration,
        must_understand=field.get("must_understand", True),
    )

to_json

Source code in src/zarr_metadata/model/_array.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    if not self.configuration and self.must_understand:
        return self.name
    out: ZarrV3NamedConfigJSON = {"name": self.name}
    if self.configuration:
        # to_json output shares no mutable state with the model.
        out["configuration"] = copy.deepcopy(self.configuration)
    if not self.must_understand:
        out["must_understand"] = False
    return out

is_array_metadata_v2

is_array_metadata_v2(
    value: object,
) -> TypeIs[ZarrV2ArrayMetadataJSON]

Whether value is a structurally-valid v2 array metadata document.

Source code in src/zarr_metadata/model/_validation.py
def is_array_metadata_v2(value: object) -> TypeIs[ZarrV2ArrayMetadataJSON]:
    """Whether `value` is a structurally-valid v2 array metadata document."""
    return (
        _is_canonical_json(value)
        and not validate_array_metadata_v2(value)
        and _is_canonical_array_metadata_v2(value)
    )

is_array_metadata_v3

is_array_metadata_v3(
    value: object,
) -> TypeIs[ZarrV3ArrayMetadataJSON]

Whether value is a structurally-valid v3 array metadata document.

Source code in src/zarr_metadata/model/_validation.py
def is_array_metadata_v3(value: object) -> TypeIs[ZarrV3ArrayMetadataJSON]:
    """Whether `value` is a structurally-valid v3 array metadata document."""
    return (
        _is_canonical_json(value)
        and not validate_array_metadata_v3(value)
        and _is_canonical_array_metadata_v3(value)
    )

is_group_metadata_v2

is_group_metadata_v2(
    value: object,
) -> TypeIs[ZarrV2GroupMetadataJSON]

Whether value is a structurally-valid v2 group metadata document.

Source code in src/zarr_metadata/model/_validation.py
def is_group_metadata_v2(value: object) -> TypeIs[ZarrV2GroupMetadataJSON]:
    """Whether `value` is a structurally-valid v2 group metadata document."""
    return _is_canonical_json(value) and not validate_group_metadata_v2(value)

is_group_metadata_v3

is_group_metadata_v3(
    value: object,
) -> TypeIs[ZarrV3GroupMetadataJSON]

Whether value is a structurally-valid v3 group metadata document.

Source code in src/zarr_metadata/model/_validation.py
def is_group_metadata_v3(value: object) -> TypeIs[ZarrV3GroupMetadataJSON]:
    """Whether `value` is a structurally-valid v3 group metadata document."""
    return _is_canonical_json(value) and not validate_group_metadata_v3(value)

is_json

is_json(value: object) -> TypeIs[JSONValue]

Whether value is a canonical JSON structure (recursively).

Source code in src/zarr_metadata/model/_validation.py
def is_json(value: object) -> TypeIs[JSONValue]:
    """Whether `value` is a canonical JSON structure (recursively)."""
    return _is_canonical_json(value)

is_metadata_field_v3

is_metadata_field_v3(
    value: object,
) -> TypeIs[ZarrV3MetadataFieldJSON]

Whether value is a v3 metadata field: a bare name or a named config.

Source code in src/zarr_metadata/model/_validation.py
def is_metadata_field_v3(value: object) -> TypeIs[ZarrV3MetadataFieldJSON]:
    """Whether `value` is a v3 metadata field: a bare name or a named config."""
    if isinstance(value, str):
        return True
    if not isinstance(value, dict):
        return False
    field = cast("dict[object, object]", value)
    return _is_canonical_json(field) and not validate_metadata_field_v3(field)

parse_array_metadata_v2

parse_array_metadata_v2(
    value: object,
) -> ZarrV2ArrayMetadataJSON

Return value as ZarrV2ArrayMetadataJSON, or raise MetadataValidationError.

Source code in src/zarr_metadata/model/_validation.py
def parse_array_metadata_v2(value: object) -> ZarrV2ArrayMetadataJSON:
    """Return `value` as `ZarrV2ArrayMetadataJSON`, or raise `MetadataValidationError`."""
    normalized = arrays_to_tuples(value)
    problems = validate_array_metadata_v2(normalized)
    if problems:
        raise MetadataValidationError(problems)
    return cast("ZarrV2ArrayMetadataJSON", normalized)

parse_array_metadata_v3

parse_array_metadata_v3(
    value: object,
) -> ZarrV3ArrayMetadataJSON

Return value as ZarrV3ArrayMetadataJSON, or raise MetadataValidationError.

Source code in src/zarr_metadata/model/_validation.py
def parse_array_metadata_v3(value: object) -> ZarrV3ArrayMetadataJSON:
    """Return `value` as `ZarrV3ArrayMetadataJSON`, or raise `MetadataValidationError`."""
    normalized = arrays_to_tuples(value)
    problems = validate_array_metadata_v3(normalized)
    if problems:
        raise MetadataValidationError(problems)
    return cast("ZarrV3ArrayMetadataJSON", normalized)

parse_group_metadata_v2

parse_group_metadata_v2(
    value: object,
) -> ZarrV2GroupMetadataJSON

Return value narrowed to ZarrV2GroupMetadataJSON, or raise MetadataValidationError.

Source code in src/zarr_metadata/model/_validation.py
def parse_group_metadata_v2(value: object) -> ZarrV2GroupMetadataJSON:
    """Return `value` narrowed to `ZarrV2GroupMetadataJSON`, or raise `MetadataValidationError`."""
    normalized = arrays_to_tuples(value)
    problems = validate_group_metadata_v2(normalized)
    if problems:
        raise MetadataValidationError(problems)
    return cast(ZarrV2GroupMetadataJSON, normalized)

parse_group_metadata_v3

parse_group_metadata_v3(
    value: object,
) -> ZarrV3GroupMetadataJSON

Return value narrowed to ZarrV3GroupMetadataJSON, or raise MetadataValidationError.

Source code in src/zarr_metadata/model/_validation.py
def parse_group_metadata_v3(value: object) -> ZarrV3GroupMetadataJSON:
    """Return `value` narrowed to `ZarrV3GroupMetadataJSON`, or raise `MetadataValidationError`."""
    normalized = arrays_to_tuples(value)
    problems = validate_group_metadata_v3(normalized)
    if problems:
        raise MetadataValidationError(problems)
    return cast(ZarrV3GroupMetadataJSON, normalized)

parse_json

parse_json(value: object) -> JSONValue

Return a canonical JSONValue, or raise MetadataValidationError.

Source code in src/zarr_metadata/model/_validation.py
def parse_json(value: object) -> JSONValue:
    """Return a canonical `JSONValue`, or raise `MetadataValidationError`."""
    normalized = arrays_to_tuples(value)
    problems = validate_json(normalized)
    if problems:
        raise MetadataValidationError(problems)
    return cast(JSONValue, normalized)

parse_metadata_field_v3

parse_metadata_field_v3(
    value: object,
) -> ZarrV3MetadataFieldJSON

Return value narrowed to ZarrV3MetadataFieldJSON, or raise MetadataValidationError.

Source code in src/zarr_metadata/model/_validation.py
def parse_metadata_field_v3(value: object) -> ZarrV3MetadataFieldJSON:
    """Return `value` narrowed to `ZarrV3MetadataFieldJSON`, or raise `MetadataValidationError`."""
    normalized = arrays_to_tuples(value)
    problems = validate_metadata_field_v3(normalized)
    if problems:
        raise MetadataValidationError(problems)
    return cast(ZarrV3MetadataFieldJSON, normalized)

validate_array_metadata_v2

validate_array_metadata_v2(
    value: object,
) -> list[ValidationProblem]

Return every reason value is not a structurally-valid v2 array doc.

Checks structure, not domain validity: dtype must be a string or field records, but the string content is not interpreted; compressor and filters are required keys that may be None, and otherwise must be codec configurations (mappings with a string id).

Source code in src/zarr_metadata/model/_validation.py
def validate_array_metadata_v2(value: object) -> list[ValidationProblem]:
    """Return every reason `value` is not a structurally-valid v2 array doc.

    Checks structure, not domain validity: `dtype` must be a string or field
    records, but the string content is not interpreted; `compressor` and
    `filters` are required keys that may be `None`, and otherwise must be
    codec configurations (mappings with a string `id`).
    """
    if not isinstance(value, Mapping):
        return [ValidationProblem((), "expected a mapping", "invalid_type")]
    doc = cast("Mapping[str, object]", value)
    problems: list[ValidationProblem] = _missing_keys(ARRAY_METADATA_REQUIRED_KEYS_V2, doc)
    problems.extend(
        _unexpected_keys(ARRAY_METADATA_STANDARD_KEYS_V2, cast("Mapping[object, object]", value))
    )
    problems.extend(_check_literal(doc, "zarr_format", 2))
    shape_problems = _validate_dim_sequence(doc, "shape")
    chunks_problems = _validate_dim_sequence(doc, "chunks")
    problems.extend(shape_problems)
    problems.extend(chunks_problems)
    if (
        not shape_problems
        and not chunks_problems
        and _is_int_sequence(doc.get("shape"))
        and _is_int_sequence(doc.get("chunks"))
    ):
        shape = cast("Sequence[int]", doc["shape"])
        chunks = cast("Sequence[int]", doc["chunks"])
        if len(shape) != len(chunks):
            problems.append(
                ValidationProblem(
                    ("chunks",),
                    "expected the same number of dimensions as shape",
                    "invalid_value",
                )
            )
    if "dtype" in doc and not _is_dtype_v2(doc["dtype"]):
        problems.append(
            ValidationProblem(
                ("dtype",),
                "expected a v2 dtype string or a sequence of field records",
                "invalid_type",
            )
        )
    if "order" in doc and doc["order"] not in ("C", "F"):
        problems.append(
            ValidationProblem(
                ("order",), f"expected 'C' or 'F', got {doc['order']!r}", "invalid_value"
            )
        )
    if "compressor" in doc:
        compressor = doc["compressor"]
        if compressor is not None:
            problems.extend(_prefix("compressor", _validate_codec_v2(compressor)))
    if "filters" in doc:
        filters = doc["filters"]
        if filters is not None and (
            isinstance(filters, str)
            or not isinstance(filters, Sequence)
            or not all(_is_codec_v2(item) for item in cast("Sequence[object]", filters))
        ):
            problems.append(
                ValidationProblem(
                    ("filters",),
                    "expected null or a sequence of codec configurations with string 'id's",
                    "invalid_type",
                )
            )
        elif filters is not None:
            if len(cast("Sequence[object]", filters)) == 0:
                problems.append(
                    ValidationProblem(("filters",), "expected at least one filter", "invalid_value")
                )
            for index, item in enumerate(cast("Sequence[object]", filters)):
                problems.extend(_prefix("filters", _prefix(index, validate_json(item))))
    if "dimension_separator" in doc and doc["dimension_separator"] not in (".", "/"):
        problems.append(
            ValidationProblem(
                ("dimension_separator",),
                f"expected '.' or '/', got {doc['dimension_separator']!r}",
                "invalid_value",
            )
        )
    if "fill_value" in doc:
        problems.extend(_prefix("fill_value", validate_json(doc["fill_value"])))
    if "attributes" in doc:
        problems.extend(_validate_attributes(doc["attributes"]))
    return problems

validate_array_metadata_v3

validate_array_metadata_v3(
    value: object,
) -> list[ValidationProblem]

Return every reason value is not a structurally-valid v3 array doc.

Checks structure, not domain validity. Unknown top-level keys are allowed (they map to extra_fields).

Source code in src/zarr_metadata/model/_validation.py
def validate_array_metadata_v3(value: object) -> list[ValidationProblem]:
    """Return every reason `value` is not a structurally-valid v3 array doc.

    Checks structure, not domain validity. Unknown top-level keys are allowed
    (they map to `extra_fields`).
    """
    if not isinstance(value, Mapping):
        return [ValidationProblem((), "expected a mapping", "invalid_type")]
    doc = cast("Mapping[str, object]", value)
    problems: list[ValidationProblem] = _missing_keys(ARRAY_METADATA_REQUIRED_KEYS_V3, doc)
    problems.extend(
        _validate_extension_fields_v3(
            cast("Mapping[object, object]", value), ARRAY_METADATA_STANDARD_KEYS_V3
        )
    )
    problems.extend(_check_literal(doc, "zarr_format", 3))
    problems.extend(_check_literal(doc, "node_type", "array"))
    problems.extend(_validate_dim_sequence(doc, "shape"))
    if "fill_value" in doc:
        problems.extend(_prefix("fill_value", validate_json(doc["fill_value"])))
    for key in ("data_type", "chunk_grid", "chunk_key_encoding"):
        if key in doc:
            problems.extend(
                _prefix(
                    key,
                    validate_metadata_field_v3(doc[key], allow_must_understand_false=False),
                )
            )
    for key in ("codecs", "storage_transformers"):
        if key in doc:
            entries = doc[key]
            if isinstance(entries, str) or not isinstance(entries, Sequence):
                problems.append(ValidationProblem((key,), "expected a sequence", "invalid_type"))
            else:
                if key == "codecs" and len(cast("Sequence[object]", entries)) == 0:
                    problems.append(
                        ValidationProblem(
                            ("codecs",), "expected at least one codec", "invalid_value"
                        )
                    )
                for index, entry in enumerate(cast("Sequence[object]", entries)):
                    problems.extend(_prefix(key, _prefix(index, validate_metadata_field_v3(entry))))
    if "attributes" in doc:
        problems.extend(_validate_attributes(doc["attributes"]))
    if "dimension_names" in doc:
        # Simple typed sequences (dimension_names, shape, chunks) report a single
        # field-level loc, not per-bad-item locs; per-index locs are reserved for
        # the metadata-field lists (codecs, storage_transformers).
        names = doc["dimension_names"]
        if isinstance(names, str) or not isinstance(names, Sequence):
            problems.append(
                ValidationProblem(("dimension_names",), "expected a sequence", "invalid_type")
            )
        elif not all(
            item is None or isinstance(item, str) for item in cast("Sequence[object]", names)
        ):
            problems.append(
                ValidationProblem(
                    ("dimension_names",), "expected items of str or None", "invalid_type"
                )
            )
        elif _is_int_sequence(doc.get("shape")) and len(cast("Sequence[object]", names)) != len(
            cast("Sequence[int]", doc["shape"])
        ):
            problems.append(
                ValidationProblem(
                    ("dimension_names",),
                    "expected one name per dimension of shape",
                    "invalid_value",
                )
            )
    return problems

validate_group_metadata_v2

validate_group_metadata_v2(
    value: object,
) -> list[ValidationProblem]

Return every reason value is not a structurally-valid v2 group doc.

Validates the in-memory merged form: the .zgroup fields plus an optional attributes mapping folded in from .zattrs.

Source code in src/zarr_metadata/model/_validation.py
def validate_group_metadata_v2(value: object) -> list[ValidationProblem]:
    """Return every reason `value` is not a structurally-valid v2 group doc.

    Validates the in-memory merged form: the `.zgroup` fields plus an
    optional `attributes` mapping folded in from `.zattrs`.
    """
    if not isinstance(value, Mapping):
        return [ValidationProblem((), "expected a mapping", "invalid_type")]
    doc = cast("Mapping[str, object]", value)
    problems: list[ValidationProblem] = _missing_keys(GROUP_METADATA_REQUIRED_KEYS_V2, doc)
    problems.extend(
        _unexpected_keys(GROUP_METADATA_STANDARD_KEYS_V2, cast("Mapping[object, object]", value))
    )
    problems.extend(_check_literal(doc, "zarr_format", 2))
    if "attributes" in doc:
        problems.extend(_validate_attributes(doc["attributes"]))
    return problems

validate_group_metadata_v3

validate_group_metadata_v3(
    value: object,
) -> list[ValidationProblem]

Return every reason value is not a structurally-valid v3 group doc.

Checks structure, not domain validity. Unknown top-level keys are allowed (they map to extra_fields); a consolidated_metadata key, if present, is deep-validated (envelope and entries) via validate_consolidated_metadata_v3.

Source code in src/zarr_metadata/model/_validation.py
def validate_group_metadata_v3(value: object) -> list[ValidationProblem]:
    """Return every reason `value` is not a structurally-valid v3 group doc.

    Checks structure, not domain validity. Unknown top-level keys are allowed
    (they map to `extra_fields`); a `consolidated_metadata` key, if present,
    is deep-validated (envelope and entries) via
    `validate_consolidated_metadata_v3`.
    """
    if not isinstance(value, Mapping):
        return [ValidationProblem((), "expected a mapping", "invalid_type")]
    doc = cast("Mapping[str, object]", value)
    problems: list[ValidationProblem] = _missing_keys(GROUP_METADATA_REQUIRED_KEYS_V3, doc)
    problems.extend(
        _validate_extension_fields_v3(
            cast("Mapping[object, object]", value),
            GROUP_METADATA_STANDARD_KEYS_V3,
            additional_reserved_keys=frozenset({"consolidated_metadata"}),
        )
    )
    problems.extend(_check_literal(doc, "zarr_format", 3))
    problems.extend(_check_literal(doc, "node_type", "group"))
    if "attributes" in doc:
        problems.extend(_validate_attributes(doc["attributes"]))
    if "consolidated_metadata" in doc and doc["consolidated_metadata"] is not None:
        # consolidated_metadata: null (a historical zarr-python bug) is
        # structurally accepted so those stores remain readable, but the model
        # repairs it to absence on read and never writes it back.
        problems.extend(
            _prefix(
                "consolidated_metadata",
                validate_consolidated_metadata_v3(doc["consolidated_metadata"]),
            )
        )
    return problems

validate_json

validate_json(value: object) -> list[ValidationProblem]

Return every reason value is not JSON-serializable (recursively).

Source code in src/zarr_metadata/model/_validation.py
def validate_json(value: object) -> list[ValidationProblem]:
    """Return every reason `value` is not JSON-serializable (recursively)."""
    if isinstance(value, float):
        if math.isfinite(value):
            return []
        return [ValidationProblem((), f"non-finite float {value!r} is not JSON", "invalid_value")]
    if isinstance(value, (str, int, bool)) or value is None:
        return []
    problems: list[ValidationProblem] = []
    if isinstance(value, Mapping):
        for key, item in cast("Mapping[object, object]", value).items():
            if not isinstance(key, str):
                problems.append(
                    ValidationProblem((), f"non-string key {key!r} in JSON object", "invalid_type")
                )
                continue
            problems.extend(_prefix(key, validate_json(item)))
        return problems
    if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray)):
        for index, item in enumerate(cast("Sequence[object]", value)):
            problems.extend(_prefix(index, validate_json(item)))
        return problems
    return [ValidationProblem((), f"not a JSON-serializable value: {value!r}", "invalid_type")]

validate_metadata_field_v3

validate_metadata_field_v3(
    value: object,
    *,
    allow_must_understand_false: bool = True,
) -> list[ValidationProblem]

Return every reason value is not a v3 metadata field.

A metadata field is a bare name string or a mapping containing name and optional configuration and must_understand members.

Source code in src/zarr_metadata/model/_validation.py
def validate_metadata_field_v3(
    value: object, *, allow_must_understand_false: bool = True
) -> list[ValidationProblem]:
    """Return every reason `value` is not a v3 metadata field.

    A metadata field is a bare name string or a mapping containing `name` and
    optional `configuration` and `must_understand` members.
    """
    if isinstance(value, str):
        return []
    if not isinstance(value, Mapping):
        return [
            ValidationProblem(
                (),
                "expected a metadata field (string or extension object)",
                "invalid_type",
            )
        ]
    field = cast("Mapping[object, object]", value)
    problems: list[ValidationProblem] = []
    allowed_keys = frozenset({"name", "configuration", "must_understand"})
    for key in field:
        if not isinstance(key, str):
            problems.append(
                ValidationProblem((), f"non-string metadata field key {key!r}", "invalid_type")
            )
        elif key not in allowed_keys:
            problems.append(
                ValidationProblem((key,), "unexpected metadata field member", "invalid_value")
            )
    if not isinstance(field.get("name"), str):
        problems.append(ValidationProblem(("name",), "expected a string name", "invalid_type"))
    if "configuration" in field:
        configuration = field["configuration"]
        if not isinstance(configuration, Mapping):
            problems.append(
                ValidationProblem(("configuration",), "expected a mapping", "invalid_type")
            )
        elif not all(isinstance(k, str) for k in cast("Mapping[object, object]", configuration)):
            problems.append(
                ValidationProblem(("configuration",), "expected string keys", "invalid_type")
            )
        else:
            for key, item in cast("Mapping[str, object]", configuration).items():
                problems.extend(_prefix("configuration", _prefix(key, validate_json(item))))
    if "must_understand" in field:
        must_understand = field["must_understand"]
        if not isinstance(must_understand, bool):
            problems.append(
                ValidationProblem(("must_understand",), "expected a boolean", "invalid_type")
            )
        elif not allow_must_understand_false and not must_understand:
            problems.append(
                ValidationProblem(
                    ("must_understand",),
                    "false is not supported at this extension point",
                    "invalid_value",
                )
            )
    return problems