Skip to content

Commit dc2f8a8

Browse files
committed
Serialize numpy scalar attributes as their Python equivalents
Setting an attribute to a numpy scalar such as np.float32(1.5), np.int64(3) or np.bool_(True) failed with "Object of type float32 is not JSON serializable". np.float64 only worked because it subclasses float. json_to_buffer now passes a default hook to json.dumps that converts numpy scalars with .item(). Other unsupported types still raise TypeError. Add a round-trip test covering both zarr formats. Fixes #3478.
1 parent df18415 commit dc2f8a8

3 files changed

Lines changed: 28 additions & 2 deletions

File tree

‎changes/3478.bugfix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Attributes holding numpy scalars, such as `np.float32(1.5)`, `np.int64(3)` or `np.bool_(True)`, can be written again. They were previously rejected with `Object of type float32 is not JSON serializable`.

‎src/zarr/core/_json.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
from __future__ import annotations
2626

2727
import json
28-
from typing import TYPE_CHECKING, cast
28+
from typing import TYPE_CHECKING, Any, cast
29+
30+
import numpy as np
2931

3032
from zarr.core.buffer import default_buffer_prototype
3133

@@ -35,6 +37,13 @@
3537
from zarr.core.common import JSON
3638

3739

40+
def _json_default(obj: Any) -> Any:
41+
"""Convert a numpy scalar, which `json` cannot encode, to the equivalent Python value."""
42+
if isinstance(obj, np.generic):
43+
return obj.item()
44+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
45+
46+
3847
def buffer_to_json(buffer: Buffer) -> JSON:
3948
"""Parse the contents of a `Buffer` as a JSON value."""
4049
# json.loads is typed as returning Any; the result is by definition JSON.
@@ -96,7 +105,9 @@ def json_to_buffer(
96105
"""
97106
if prototype is None:
98107
prototype = default_buffer_prototype()
99-
return prototype.buffer.from_bytes(json.dumps(obj, indent=indent, allow_nan=allow_nan).encode())
108+
return prototype.buffer.from_bytes(
109+
json.dumps(obj, indent=indent, allow_nan=allow_nan, default=_json_default).encode()
110+
)
100111

101112

102113
async def get_json(store: Store, key: str, *, byte_range: ByteRequest | None = None) -> JSON | None:

‎tests/test_attributes.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ def test_put(data: dict[str, Any], zarr_format: ZarrFormat) -> None:
2626
assert deep_nan_equal(dict(attrs), expected)
2727

2828

29+
@pytest.mark.parametrize("zarr_format", [2, 3])
30+
@pytest.mark.parametrize(
31+
"value",
32+
[np.float32(1.5), np.float32(np.nan), np.int64(3), np.uint8(7), np.bool_(True)],
33+
)
34+
def test_numpy_scalar_attribute_roundtrip(value: Any, zarr_format: ZarrFormat) -> None:
35+
"""numpy scalars are stored as their Python equivalents and survive reopening the store."""
36+
store = zarr.storage.MemoryStore()
37+
zarr.create_group(store, zarr_format=zarr_format, attributes={"foo": value})
38+
reopened = zarr.open_group(store, mode="r", zarr_format=zarr_format)
39+
expected = value.item()
40+
assert deep_nan_equal({"foo": reopened.attrs["foo"]}, {"foo": expected})
41+
42+
2943
def test_asdict() -> None:
3044
store = zarr.storage.MemoryStore()
3145
attrs = zarr.core.attributes.Attributes(

0 commit comments

Comments
 (0)