Skip to content

Commit b1d4132

Browse files
authored
fix(zarr-metadata): make JSONValue's array arm covariant (#4264)
* fix(zarr-metadata): make JSONValue's array arm covariant `list["JSONValue"] | tuple["JSONValue", ...]` is invariant in the element type, so a value typed with any narrower element -- a `list[str]` field on a TypedDict, a `Sequence[float]` -- was not assignable to `JSONValue`, and a TypedDict carrying such fields was not assignable to `Mapping[str, JSONValue]`. pyright's diagnostic for the failure suggests the fix verbatim: "Consider switching from list to Sequence which is covariant." The array arm is now `Sequence["JSONValue"]`. The docstring records the deliberate type-level cost (`Sequence` admits `str`/`bytes`; runtime narrowing must exclude them regardless of the alias's spelling). Found while aliasing zarr-cm's JsonValue to this type: the two aliases are structurally identical except for this arm, and with it changed, pyright unifies them across the package boundary. Assisted-by: ClaudeCode:claude-opus-5 * Rename 295.bugfix.md to 4264.bugfix.md
1 parent 12aa83b commit b1d4132

2 files changed

Lines changed: 24 additions & 9 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
`JSONValue`'s array arm is now the covariant `Sequence["JSONValue"]` rather
2+
than the invariant `list["JSONValue"] | tuple["JSONValue", ...]`. Values typed
3+
with a narrower element type — a `list[str]` field on a TypedDict, a
4+
`Sequence[float]` — now count as JSON values, and TypedDicts whose fields
5+
carry precise types are now assignable to `Mapping[str, JSONValue]`.
6+
Type-level cost, accepted deliberately: `Sequence` says nothing about the
7+
concrete container and admits `str`/`bytes`, so runtime code narrowing a JSON
8+
array must exclude `str`/`bytes`/`bytearray` — as it already had to, since
9+
`str` was always a union arm.

‎packages/zarr-metadata/src/zarr_metadata/_common.py‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,34 @@
66
`zarr_metadata.v3.data_type`.
77
"""
88

9-
from collections.abc import Mapping
9+
from collections.abc import Mapping, Sequence
1010
from typing import NotRequired
1111

1212
from typing_extensions import TypeAliasType, TypedDict
1313

1414
JSONValue = TypeAliasType(
1515
"JSONValue",
16-
int
17-
| float
18-
| bool
19-
| str
20-
| list["JSONValue"]
21-
| tuple["JSONValue", ...]
22-
| Mapping[str, "JSONValue"]
23-
| None,
16+
int | float | bool | str | Sequence["JSONValue"] | Mapping[str, "JSONValue"] | None,
2417
)
2518
"""A recursive type alias for JSON-encodable values.
2619
2720
Defined via `TypeAliasType` (rather than a plain `TypeAlias`) so the
2821
self-reference is a named recursion point that pydantic can resolve when
2922
building a `TypeAdapter`; a bare recursive `TypeAlias` raises
3023
`PydanticUserError`/`RecursionError` at validation time.
24+
25+
The array arm is the covariant `Sequence` rather than the invariant
26+
`list["JSONValue"] | tuple["JSONValue", ...]`, so values typed with a
27+
*narrower* element type still count as JSON values: a `list[str]` field on a
28+
TypedDict is assignable to `JSONValue` under `Sequence` but not under
29+
`list[JSONValue]` (`list` is invariant in its element type, and pyright's
30+
diagnostic for that failure suggests exactly this change). This is what lets
31+
downstream TypedDicts give their fields precise types (`Sequence[str]`,
32+
`list[int]`, ...) while remaining assignable to `Mapping[str, JSONValue]`.
33+
The type-level cost, accepted deliberately: `Sequence` says nothing about the
34+
concrete container, and it admits `str`/`bytes` (`str` was already a union
35+
arm); runtime code narrowing a JSON array must exclude `str`/`bytes`/
36+
`bytearray` regardless of how this alias is spelled.
3137
"""
3238

3339

0 commit comments

Comments
 (0)