Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/zarr-metadata/changes/4264.bugfix.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 15 additions & 9 deletions packages/zarr-metadata/src/zarr_metadata/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,34 @@
`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.

Defined via `TypeAliasType` (rather than a plain `TypeAlias`) so the
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.
"""


Expand Down
Loading