diff --git a/packages/zarr-metadata/changes/4264.bugfix.md b/packages/zarr-metadata/changes/4264.bugfix.md new file mode 100644 index 0000000000..466bf5941d --- /dev/null +++ b/packages/zarr-metadata/changes/4264.bugfix.md @@ -0,0 +1,9 @@ +`JSONValue`'s array arm is now the covariant `Sequence["JSONValue"]` rather +than the invariant `list["JSONValue"] | tuple["JSONValue", ...]`. Values typed +with a narrower element type — a `list[str]` field on a TypedDict, a +`Sequence[float]` — now count as JSON values, and TypedDicts whose fields +carry precise types are now assignable to `Mapping[str, JSONValue]`. +Type-level cost, accepted deliberately: `Sequence` says nothing about the +concrete container and admits `str`/`bytes`, so runtime code narrowing a JSON +array must exclude `str`/`bytes`/`bytearray` — as it already had to, since +`str` was always a union arm. diff --git a/packages/zarr-metadata/src/zarr_metadata/_common.py b/packages/zarr-metadata/src/zarr_metadata/_common.py index f3259f7b73..08c143107f 100644 --- a/packages/zarr-metadata/src/zarr_metadata/_common.py +++ b/packages/zarr-metadata/src/zarr_metadata/_common.py @@ -6,21 +6,14 @@ `zarr_metadata.v3.data_type`. """ -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from typing import NotRequired from typing_extensions import TypeAliasType, TypedDict JSONValue = TypeAliasType( "JSONValue", - int - | float - | bool - | str - | list["JSONValue"] - | tuple["JSONValue", ...] - | Mapping[str, "JSONValue"] - | None, + int | float | bool | str | Sequence["JSONValue"] | Mapping[str, "JSONValue"] | None, ) """A recursive type alias for JSON-encodable values. @@ -28,6 +21,19 @@ self-reference is a named recursion point that pydantic can resolve when building a `TypeAdapter`; a bare recursive `TypeAlias` raises `PydanticUserError`/`RecursionError` at validation time. + +The array arm is the covariant `Sequence` rather than the invariant +`list["JSONValue"] | tuple["JSONValue", ...]`, so values typed with a +*narrower* element type still count as JSON values: a `list[str]` field on a +TypedDict is assignable to `JSONValue` under `Sequence` but not under +`list[JSONValue]` (`list` is invariant in its element type, and pyright's +diagnostic for that failure suggests exactly this change). This is what lets +downstream TypedDicts give their fields precise types (`Sequence[str]`, +`list[int]`, ...) while remaining assignable to `Mapping[str, JSONValue]`. +The type-level cost, accepted deliberately: `Sequence` says nothing about the +concrete container, and it admits `str`/`bytes` (`str` was already a union +arm); runtime code narrowing a JSON array must exclude `str`/`bytes`/ +`bytearray` regardless of how this alias is spelled. """