Skip to content

Bugs turned up while creating the zarr-metadata type checker #4453

Open

Description

@d-v-b

Here's a list of bugs I found while working on the zarr-metadata type checker (see #4447). I had claude write this up.

🤖 AI text below 🤖

These were found by differential testing of zarr-python against the zarr-metadata validators (d-v-b#370–#382), and every item was re-run against zarr 3.4.1.dev50+gf3bfd68a6 (main as of 2026-09-29) with numpy 2.5.3. Spec references are to zarr-specs at fc7dd9c9. Grouped by consequence, worst first. Repros use this array document as the base and replace one member:

ARRAY = {
    "zarr_format": 3, "node_type": "array", "shape": [1], "data_type": "int8",
    "chunk_grid": {"name": "regular", "configuration": {"chunk_shape": [1]}},
    "chunk_key_encoding": {"name": "default"}, "fill_value": 0,
    "codecs": [{"name": "bytes", "configuration": {"endian": "little"}}],
}
GROUP = {"zarr_format": 3, "node_type": "group", "attributes": {}}

1. Reserved node names are written, and one of them makes the hierarchy unreadable

Node names must not start with __ and must not be zarr.json (core L818–L837).

  • root.create_group("__a") succeeds, writes __a/zarr.json, and root.members() lists __a.
  • root.create_group("a/zarr.json") succeeds and writes a/zarr.json/zarr.json — a directory named zarr.json. root.members() is then []: a has no document of its own, so it and everything under it is invisible.
  • root.create_group("zarr.json") on a LocalStore fails with FileExistsError: [Errno 17] File exists: '.../zarr.json' (the directory cannot be created over the root's document) and leaves a stray zarr.<uuid>.partial file behind; on a MemoryStore it writes zarr.json/zarr.json. The name is not refused before writing starts.

2. Consolidated metadata: invalid listings are accepted, and one bad entry hides the whole group

ConsolidatedMetadata.from_dict and GroupMetadata.from_dict accept metadata entries keyed "", ".", "..", "zarr.json", "__a", and entries that are v2 documents ({"zarr_format": 2, "attributes": {}}); Group.members() then lists '' and '__a' as children. Keys are node paths, made of node names (core L211–L229).

  • A child listed without its parent ({"a/b": GROUP} only): from_dict raises KeyError: 'a', and zarr.open_group(store) on a store holding that document raises GroupNotFoundError: No group found in store ... at path '' — the root group itself becomes unopenable because of one listing entry.
  • A node listed below an array ({"arr": ARRAY, "arr/child": GROUP}): accepted by from_dict and by open_group; members() (and members(max_depth=None)) raises KeyError: 'arr/child'. "Group nodes may have children but array nodes may not" (core L177–L181).

3. Float fill values: equality conflates distinct values, and NaN payloads are lost on a round trip

  • ArrayV3Metadata.from_dict({**ARRAY, "data_type": "float32", "fill_value": -0.0}) == ...fill_value: 0.0 is True, though the two are written back as -0.0 and 0.0: two documents that differ compare equal.
  • fill_value: "0x7fc00001" (a float32 NaN with payload 1) is read, written back as "NaN", and compares equal to a plain NaN. The spec writes a NaN other than the canonical one as its hex bits (data-types L63–L79).

4. Fill-value handling at the edges

  • An out-of-range integer fill leaks numpy's exception: fill_value: 300 for int8 → OverflowError: Python integer 300 out of bounds for int8 (-129 likewise), where a validation error is expected.
  • The byte-array form of a bytes fill value is refused: fill_value: [65] → TypeError: Invalid type: [65]. Expected a string. for bytes and variable_length_bytes (... Expected a base64-encoded string. for null_terminated_bytes); only the base64 form is taken. The spec allows an array of byte values (data-types L97–L99).
  • The r* raw-bits data types are not supported: data_type: "r16" → ValueError: No Zarr data type found that matches 'r16'.

5. Documents accepted that the spec rules out

Each is accepted by ArrayV3Metadata.from_dict (and so by open_array) without any error, with the base document above and the one member replaced:

member value why it is invalid
shape [true] a boolean is not an integer
dimension_names null an array, with a string or null per dimension
attributes [] an object
storage_transformers null an array
codecs [gzip, bytes] one array→bytes codec, with bytes→bytes codecs only after it (core L974–L983, L1048–L1050)
sharding index_codecs [] the index needs an array→bytes codec (sharding L129–L138)
sharding index_codecs [{"name": "bytes"}] endian is required for the uint64 index (bytes L64–L69)
codecs [vlen-utf8] on int8 a string codec on an integer type
crc32c {"configuration": {"bogus": 1}} crc32c takes no configuration members (crc32c L63–L66; unknown members, core L1562–L1564)
zstd level: -131073 one below zstd's minimum level, -131072
fill_value (bool) null, "true", 1 a boolean; null is silently read as false
fill_value (float32) "nan", "inf", true a number, or one of "NaN", "Infinity", "-Infinity", or hex bits (data-types L63–L79, L88–L91)
fill_value (structured {a: int8, b: int8}) {"a": 1} / {"a": 1, "b": 2, "c": 3} read as {"a": 1, "b": 0} / {"a": 1, "b": 2}: a missing field defaults silently, an extra one is dropped

Context

The zarr-metadata package on the d-v-b/zarr-python fork (PRs #370–#382, to be proposed upstream as a single PR) refuses each of these with a located problem and the spec citation above; the adversarial-review pass that produced this list is described in d-v-b#382. Happy to split any group into its own issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions