Skip to content

Commit f83d661

Browse files
committed
fix(zarr-metadata): a v2 array's other members are JSON with string keys
The v2 array validator tolerates members outside the `.zarray` definition, as the spec asks readers to ignore them, but never looked at them: `parse_array_metadata_v2` returned a document with an integer key, or with a set or NaN as a member, typed as `ZarrV2ArrayMetadataJSON`, where `is_array_metadata_v2` said no. The members are now checked as v3 checks its extension fields, by the same helper, renamed for both. Assisted-by: ClaudeCode:claude-opus-5-5
1 parent b3045a2 commit f83d661

4 files changed

Lines changed: 55 additions & 8 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
A v2 array document's members outside the `.zarray` definition are still
2+
tolerated, and dropped on read, but one that is not a JSON value, or whose
3+
key is not a string, is now a problem, as it is in a v3 document.
4+
`parse_array_metadata_v2` returned such members in a document its type
5+
says holds string keys, which `is_array_metadata_v2` refused.
6+
`ZarrV2ArrayMetadataJSON` declares its other members as JSON values
7+
(`extra_items`), as its pydantic schema already did.

‎packages/zarr-metadata/src/zarr_metadata/model/_validation.py‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,17 @@ def _check_literal(
279279
return ()
280280

281281

282-
def _validate_extension_fields_v3(
282+
def _validate_other_members(
283283
doc: Mapping[object, object],
284284
standard_keys: frozenset[str],
285285
*,
286286
additional_reserved_keys: frozenset[str] = frozenset(),
287287
) -> tuple[ValidationProblem, ...]:
288-
"""Validate v3 top-level key types and unknown-field JSON payloads."""
288+
"""Every key a string, and every member outside `standard_keys` a JSON value.
289+
290+
For a document open to other members: v3 extension fields, and the
291+
members a v2 array's readers ignore.
292+
"""
289293
problems: list[ValidationProblem] = []
290294
reserved_keys = standard_keys | additional_reserved_keys
291295
for key, value in doc.items():
@@ -558,7 +562,7 @@ def validate_array_metadata_v3(value: object) -> tuple[ValidationProblem, ...]:
558562
doc = cast("Mapping[str, object]", value)
559563
problems: list[ValidationProblem] = list(_missing_keys(ARRAY_METADATA_REQUIRED_KEYS_V3, doc))
560564
problems.extend(
561-
_validate_extension_fields_v3(
565+
_validate_other_members(
562566
cast("Mapping[object, object]", value), ARRAY_METADATA_STANDARD_KEYS_V3
563567
)
564568
)
@@ -653,9 +657,15 @@ def validate_array_metadata_v2(value: object) -> tuple[ValidationProblem, ...]:
653657
# Unlike the group document ("Other keys MUST NOT be present",
654658
# https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v2/v2.0.rst#L313), the v2 array document is open: other keys "SHOULD NOT be
655659
# present within the metadata object and SHOULD be ignored by
656-
# implementations" (https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v2/v2.0.rst#L91-L92), so members outside
657-
# ARRAY_METADATA_STANDARD_KEYS_V2 are not problems.
660+
# implementations" (https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v2/v2.0.rst#L91-L92), so a member outside
661+
# ARRAY_METADATA_STANDARD_KEYS_V2 is not a problem for being there. Ignored
662+
# is not unchecked: it is JSON, and its key a string, as in v3.
658663
problems: list[ValidationProblem] = list(_missing_keys(ARRAY_METADATA_REQUIRED_KEYS_V2, doc))
664+
problems.extend(
665+
_validate_other_members(
666+
cast("Mapping[object, object]", value), ARRAY_METADATA_STANDARD_KEYS_V2
667+
)
668+
)
659669
problems.extend(_check_literal(doc, "zarr_format", 2))
660670
shape_problems = _validate_dim_sequence(doc, "shape")
661671
chunks_problems = _validate_dim_sequence(doc, "chunks")
@@ -819,7 +829,7 @@ def validate_group_metadata_v3(value: object) -> tuple[ValidationProblem, ...]:
819829
doc = cast("Mapping[str, object]", value)
820830
problems: list[ValidationProblem] = list(_missing_keys(GROUP_METADATA_REQUIRED_KEYS_V3, doc))
821831
problems.extend(
822-
_validate_extension_fields_v3(
832+
_validate_other_members(
823833
cast("Mapping[object, object]", value),
824834
GROUP_METADATA_STANDARD_KEYS_V3,
825835
additional_reserved_keys=frozenset({"consolidated_metadata"}),

‎packages/zarr-metadata/src/zarr_metadata/v2/array.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class ZarrV2ZArrayJSON(TypedDict):
7777
dimension_separator: NotRequired[ZarrV2ArrayDimensionSeparator]
7878

7979

80-
class ZarrV2ArrayMetadataJSON(TypedDict):
80+
class ZarrV2ArrayMetadataJSON(TypedDict, extra_items=JSONValue):
8181
"""
8282
Zarr v2 array metadata document, in-memory merged form.
8383
@@ -89,6 +89,10 @@ class ZarrV2ArrayMetadataJSON(TypedDict):
8989
real `.zarray` file should split / merge `attributes` accordingly,
9090
or use `ZarrV2ZArrayJSON` (strict on-disk) plus `ZarrV2ZAttrsJSON` directly.
9191
92+
Open: other keys "SHOULD NOT be present ... and SHOULD be ignored by
93+
implementations" (https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v2/v2.0.rst#L91-L92), and each
94+
other member is a JSON value.
95+
9296
See https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html
9397
"""
9498

@@ -108,7 +112,7 @@ class ZarrV2ArrayMetadataJSON(TypedDict):
108112
"""
109113

110114

111-
class ZarrV2ArrayMetadataJSONPartial(TypedDict, total=False):
115+
class ZarrV2ArrayMetadataJSONPartial(TypedDict, total=False, extra_items=JSONValue):
112116
"""
113117
Partial form of `ZarrV2ArrayMetadataJSON`: every field is `NotRequired`.
114118

‎packages/zarr-metadata/tests/model/test_array.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import copy
44
import dataclasses
55
import json
6+
import math
67
from collections import UserDict
78
from collections.abc import Callable
89
from typing import TYPE_CHECKING, get_args
@@ -1392,6 +1393,31 @@ def test_array_v2_ignores_unknown_document_member() -> None:
13921393
assert "unexpected" not in ZarrV2ArrayMetadata.from_json(doc).to_json()
13931394

13941395

1396+
@pytest.mark.parametrize(
1397+
("member", "kind"),
1398+
[
1399+
(object(), "invalid_type"),
1400+
({1, 2}, "invalid_type"),
1401+
(b"\x00", "invalid_type"),
1402+
(math.nan, "invalid_value"),
1403+
],
1404+
ids=["object", "set", "bytes", "nan"],
1405+
)
1406+
def test_error_array_v2_unknown_member_that_is_not_json(member: object, kind: str) -> None:
1407+
"""Ignored is not unchecked: an unknown member is a JSON value, as in v3."""
1408+
doc = dict(ZarrV2ArrayMetadata.create_default().to_json()) | {"unexpected": member}
1409+
1410+
assert [(p.loc, p.kind) for p in validate_array_metadata_v2(doc)] == [(("unexpected",), kind)]
1411+
1412+
1413+
@pytest.mark.parametrize("key", [7, None, True, (1, 2)], ids=["int", "none", "bool", "tuple"])
1414+
def test_error_array_v2_key_that_is_not_a_string(key: object) -> None:
1415+
"""A document's keys are strings: `parse_array_metadata_v2` returned this one."""
1416+
doc: dict[object, object] = {**ZarrV2ArrayMetadata.create_default().to_json(), key: "x"}
1417+
1418+
assert [(p.loc, p.kind) for p in validate_array_metadata_v2(doc)] == [((), "invalid_type")]
1419+
1420+
13951421
def test_array_v3_from_json_materializes_abstract_containers() -> None:
13961422
"""A flexible input mapping becomes the canonical dict/tuple model shape."""
13971423
doc = UserDict(dict(ZarrV3ArrayMetadata.create_default(shape=(2,)).to_json()))

0 commit comments

Comments
 (0)