Skip to content

Commit 57bf497

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 4aeac75 commit 57bf497

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
@@ -221,13 +221,17 @@ def _check_literal(
221221
return ()
222222

223223

224-
def _validate_extension_fields_v3(
224+
def _validate_other_members(
225225
doc: Mapping[object, object],
226226
standard_keys: frozenset[str],
227227
*,
228228
additional_reserved_keys: frozenset[str] = frozenset(),
229229
) -> tuple[ValidationProblem, ...]:
230-
"""Validate v3 top-level key types and unknown-field JSON payloads."""
230+
"""Every key a string, and every member outside `standard_keys` a JSON value.
231+
232+
For a document open to other members: v3 extension fields, and the
233+
members a v2 array's readers ignore.
234+
"""
231235
problems: list[ValidationProblem] = []
232236
reserved_keys = standard_keys | additional_reserved_keys
233237
for key, value in doc.items():
@@ -500,7 +504,7 @@ def validate_array_metadata_v3(value: object) -> tuple[ValidationProblem, ...]:
500504
doc = cast("Mapping[str, object]", value)
501505
problems: list[ValidationProblem] = list(_missing_keys(ARRAY_METADATA_REQUIRED_KEYS_V3, doc))
502506
problems.extend(
503-
_validate_extension_fields_v3(
507+
_validate_other_members(
504508
cast("Mapping[object, object]", value), ARRAY_METADATA_STANDARD_KEYS_V3
505509
)
506510
)
@@ -595,9 +599,15 @@ def validate_array_metadata_v2(value: object) -> tuple[ValidationProblem, ...]:
595599
# Unlike the group document ("Other keys MUST NOT be present",
596600
# 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
597601
# present within the metadata object and SHOULD be ignored by
598-
# implementations" (https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v2/v2.0.rst#L91-L92), so members outside
599-
# ARRAY_METADATA_STANDARD_KEYS_V2 are not problems.
602+
# implementations" (https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v2/v2.0.rst#L91-L92), so a member outside
603+
# ARRAY_METADATA_STANDARD_KEYS_V2 is not a problem for being there. Ignored
604+
# is not unchecked: it is JSON, and its key a string, as in v3.
600605
problems: list[ValidationProblem] = list(_missing_keys(ARRAY_METADATA_REQUIRED_KEYS_V2, doc))
606+
problems.extend(
607+
_validate_other_members(
608+
cast("Mapping[object, object]", value), ARRAY_METADATA_STANDARD_KEYS_V2
609+
)
610+
)
601611
problems.extend(_check_literal(doc, "zarr_format", 2))
602612
shape_problems = _validate_dim_sequence(doc, "shape")
603613
chunks_problems = _validate_dim_sequence(doc, "chunks")
@@ -761,7 +771,7 @@ def validate_group_metadata_v3(value: object) -> tuple[ValidationProblem, ...]:
761771
doc = cast("Mapping[str, object]", value)
762772
problems: list[ValidationProblem] = list(_missing_keys(GROUP_METADATA_REQUIRED_KEYS_V3, doc))
763773
problems.extend(
764-
_validate_extension_fields_v3(
774+
_validate_other_members(
765775
cast("Mapping[object, object]", value),
766776
GROUP_METADATA_STANDARD_KEYS_V3,
767777
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
@@ -1403,6 +1404,31 @@ def test_array_v2_ignores_unknown_document_member() -> None:
14031404
assert "unexpected" not in ZarrV2ArrayMetadata.from_json(doc).to_json()
14041405

14051406

1407+
@pytest.mark.parametrize(
1408+
("member", "kind"),
1409+
[
1410+
(object(), "invalid_type"),
1411+
({1, 2}, "invalid_type"),
1412+
(b"\x00", "invalid_type"),
1413+
(math.nan, "invalid_value"),
1414+
],
1415+
ids=["object", "set", "bytes", "nan"],
1416+
)
1417+
def test_error_array_v2_unknown_member_that_is_not_json(member: object, kind: str) -> None:
1418+
"""Ignored is not unchecked: an unknown member is a JSON value, as in v3."""
1419+
doc = dict(ZarrV2ArrayMetadata.create_default().to_json()) | {"unexpected": member}
1420+
1421+
assert [(p.loc, p.kind) for p in validate_array_metadata_v2(doc)] == [(("unexpected",), kind)]
1422+
1423+
1424+
@pytest.mark.parametrize("key", [7, None, True, (1, 2)], ids=["int", "none", "bool", "tuple"])
1425+
def test_error_array_v2_key_that_is_not_a_string(key: object) -> None:
1426+
"""A document's keys are strings: `parse_array_metadata_v2` returned this one."""
1427+
doc: dict[object, object] = {**ZarrV2ArrayMetadata.create_default().to_json(), key: "x"}
1428+
1429+
assert [(p.loc, p.kind) for p in validate_array_metadata_v2(doc)] == [((), "invalid_type")]
1430+
1431+
14061432
def test_array_v3_from_json_materializes_abstract_containers() -> None:
14071433
"""A flexible input mapping becomes the canonical dict/tuple model shape."""
14081434
doc = UserDict(dict(ZarrV3ArrayMetadata.create_default(shape=(2,)).to_json()))

0 commit comments

Comments
 (0)