Conversation
…nknown key wherever it sits `ProblemKind` defines `unknown_key` as a key an object's type does not declare where the type is closed, and a v3 configuration reported one so. A v2 `.zgroup`, a `.zmetadata` envelope, a group's `consolidated_metadata` and a metadata field's envelope reported the same case as `invalid_value`, so a reader that tolerates what another writer added -- netCDF-C's `_nczarr_*` keys (zarr-python#2296) -- could not filter it by kind. Each is now `unknown_key`, with the checker's message, "unexpected key 'x'". A definition's `check` and `judge` give a configuration back when a field it holds has a stray member, reported and left out, as the checker leaves out a key a closed TypedDict does not declare; a `must_understand` of `false` still refuses it. Assisted-by: ClaudeCode:claude-opus-5-5
…s in `read_node_metadata_v3` reads a document by the node type it names, and one that names none was reported only as missing its `node_type`. A crawler meeting the root `zarr.json` of zarr-python 2's draft of v3 (zarr-python#2982), whose `zarr_format` is a URL, learned "no node type", not "not v3". A document the dispatch cannot follow is now judged by its `zarr_format` as well: missing, or other than 3, is a problem beside the node type's. Assisted-by: ClaudeCode:claude-opus-5-5
…below its group
A group's consolidated metadata holds the hierarchy below the group:
zarr-python keeps the document of the node at /a/b of that hierarchy,
the group its root, at the key a/b. Nothing judged the keys or the tree
they make. A key that is no node's path ("", "__a", "a/../b", "/a"), a
document below an array's, and a document whose group is missing were
read without a problem; zarr-python raises a bare KeyError on three of
them and keeps the rest. Each is now a problem at its entry, a missing
group a missing_key, and ZarrV3ConsolidatedMetadata refuses them when it
is built. A listed group's own listing lists what the group lists, at
the joined key and of the same node type: a node it lists alone is
dropped by zarr-python's reader, which keeps the flat listing.
The rules are a hierarchy's, not consolidated metadata's: a private
hierarchy_problems judges the node type of each node, by its node path,
as the spec's tree, so a model of a whole hierarchy can use it too. Every
problem is one per value, saying its reasons at once -- a path of a
thousand bad names is one problem, a chain of missing groups one
missing_key at the nearest -- so what is reported weighs what was read.
NodeName and NodePath, in zarr_metadata.v3, are the spec's node names
and paths, modelled on zarrs' types, and zarr_metadata.model judges a
string by them with validate/is/parse_node_name_v3 and their node_path
twins.
Assisted-by: ClaudeCode:claude-opus-5-5
Assisted-by: ClaudeCode:claude-fable-5-1
…ment A v3 array model compared its members with ==, so "NaN" and "0x7fc00000" were two float32 fill values, 0.0 and -0.0 one, a blosc with and without the typesize that noshuffle ignores two codecs though canonical_of spelled them alike, and a model holding a NaN attribute never equalled its own copy. Two models are now equal when they mean the same document: what the package interprets compares by its canonical spelling, and what it does not compares as JSON text, which tells true from 1 and -0.0 from 0.0 and takes NaN for itself. A data type's definition says which spelling of a fill value is its value's own, DataTypeDefinition.fill_value_canonical, and canonical_fill_value spells one, or gives UNSET for a fill value with a problem: the float types read a number as a float64, as JSON parsers and numpy do, and spell a value by its shortest number, a named value, or a NaN's bits; complex by part; the numpy time types' -2**63 as "NaT"; bytes as base64; a struct field by field. A field, Read, Unclaimed or Refused, compares by field_key: its definition and its canonical configuration as text, with the fields it holds by their own keys; default, v2, sharding_indexed and zstd gain a canonical folding their spec defaults. Every model and field hashes as it compares. Assisted-by: ClaudeCode:claude-opus-5-5 Assisted-by: ClaudeCode:claude-fable-5-1
d-v-b
force-pushed
the
fix/zarr-metadata-foreign-document-gaps
branch
from
September 30, 2026 08:18
b260b89 to
f5753be
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI text below 🤖
A member a closed object does not declare is an
unknown_keywhereverit sits, as
ProblemKinddefines one: in a v2.zgroup, a.zmetadataenvelope, a group's
consolidated_metadataand a metadata field'senvelope, where it was an
invalid_value, as it already was inside a v3configuration. So a reader that tolerates a key another writer added,
such as netCDF-C's
_nczarr_*keys in a.zgroup, can filter by kind.A definition's
checkandjudgegive a configuration back when afield it holds has a stray member, which they report and leave out, as
the checker does a key a closed TypedDict does not declare.
A
zarr.jsonthat says no node type the spec defines is judged by itszarr_formattoo, so a document of another format says it is not v3.The root
zarr.jsonof zarr-python 2's draft of v3, whosezarr_formatis a URL, was reported only as missing its
node_type; it now also hasan
invalid_typeproblem atzarr_format, and a v2 document read as azarr.jsonaninvalid_valuethere.A group's consolidated metadata is judged as the hierarchy below the
group, the group its root: zarr-python keeps the document of the node at
/a/bat the keya/b, and the documents and the group make a tree inwhich only groups hold nodes and each node's parent is held. A key that
is no node's path below the group, such as
"","__a"or"a/../b",and a document below an array's were read without a problem, and so was
a document whose group is missing, which zarr-python fails to read. Each
is a problem now, a missing group a
missing_key, andZarrV3ConsolidatedMetadatarefuses them when it is built.Two v3 array models are equal when their fill values are one value of the
data type, however each is spelled.
"NaN"and"0x7fc00000"were twofloat32fill values, and0.0and-0.0, which are two values of afloat, were one, since
==compared the JSON. A fill value whose datatype is not interpreted -- one nothing in scope claims, or a v2 model's
-- is compared as before.
NodeNameandNodePath, inzarr_metadata.v3, are the names and pathsof the nodes of a v3 hierarchy, modelled on zarrs' types of those names,
and
zarr_metadata.modeljudges a string by the spec's rules for them:validate_node_name_v3,is_node_name_v3andparse_node_name_v3, andtheir
node_pathtwins.A data type's definition says which spelling of a fill value is its
value's own:
DataTypeDefinition.fill_value_canonical, andcanonical_fill_value(data_type, value)inzarr_metadata.v3.definitionspells one, or gives
UNSETfor a fill value with a problem. The floattypes read a number as a float64, as JSON parsers and numpy do, and
spell a value by its shortest number, a named value, or the bits of a
NaN the spec does not name; the complex types spell each part so; the
numpy time types spell
-2**63as"NaT";bytesspells its bytes asbase64; and a struct spells each field's fill value by that field's
type.
What changes for a caller
_nczarr_groupin a.zgroupinvalid_valueunknown_key, "unexpected key '_nczarr_group'"judge/checkof a configuration whose nested field has a stray memberNonezarr.jsonnode_typemissingzarr_format:invalid_type, "expected 3, got" the URL"__a","","a/../b"or"/a"invalid_valueat the entrya/bwhereais an arrayinvalid_valueata/b: 'expected a node below a group, got "/a/b", below the array "/a"'a/b/cwith noa/bKeyError)missing_keyata/bfloat32fill"NaN"vs"0x7fc00000"float32fill0.0vs-0.0How it works
Kinds.
unexpected_keysand the envelope check report a stray member asunknown_key.judgeputs each nested field back with only the members its envelope declares, asresolvealready did.Format. When the node-type dispatch cannot follow a document,
_node_typejudges itszarr_formattoo, for the reader and the validator alike.Hierarchy. A private
hierarchy_problems(nodes)inv3/_hierarchy.pytakes the node type of each node by its node path. It judges them as the spec's tree:It knows nothing of consolidated metadata, so a future model of a whole hierarchy can use it as is. Consolidated metadata is one caller: the group is the root
/, and the keya/bis the node at/a/b. A key's own form is judged where the entry is read, before the entry's problems. The tree is judged once every entry is read.Fill values.
DataTypeDefinition.fill_value_canonicalis a hook likefill_value_rules, defaulting to "as written".ZarrV3ArrayMetadata.__eq__compares the canonical spellings as JSON text, and__hash__agrees with it. The spelling is computed only when models are compared, so reads cost nothing more. For floats:float_bitsgives the value's bits in the type;Verification
Tests. 1,804 pass on Python 3.14, and 1,802 plus 2 skipped on 3.11. Each of the four commits passes on its own. pyright, ruff and the strict docs build are clean.
Mutation checks. Five deliberate breakages of the float code each fail the tests: the named NaN spelled as bits, the sign lost on either overflow path, the neighbouring shortest decimal not tried, and the signed-zero guard dropped. Two first survived, the sign on the integer overflow path and the signed-zero guard, so the agent added a float16
-0.0row and a negative integer past the float64 range.Canonical spellings against numpy. All float16 values, every float32 power of two and 200,000 random float32 values: 0 mismatches with numpy's shortest repr.
Differential. 481,228 records, compared between feat(zarr-metadata)!: check each model when it is built, and write it as it is #379 and this PR. Every difference is explained by one of the fixes:
zarr_formatproblem: 21 records;a/bwithouta.The fill-value change moves no record.
Real documents. 8,808
zarr.jsonfiles: the scenarios' stores, test corpora, and documents fetched or regenerated from zarr-python issues. 171 hold consolidated metadata, 121 of them with nested paths and 1,005 entries in all. The only change is the expected one: the two draft-v3 roots now report theirzarr_format. No consolidated document is newly refused.Scenarios. The four earlier scenarios replay unchanged. A fifth, a catalog reader of 18 stores that other tools wrote (NCZarr, n5-zarr, IDR, EMBL, earthkit, tensorstore, kerchunk, zarr 2.18 to 3.1), was built for this PR and replayed on it:
0.0from-0.0.Reviews
roborev, one pass per commit. It found:
judgegave a nested field back with its stray member;zarr_formatchange;All are fixed. It also questioned the
zarr.jsonnode-name rule; the pinned spec lists it (core L818-L837).Design review. It found:
canonical_fill_valuereturnedNoneboth for a problem and for a JSONnullfill value;__hash__that disagreed with__eq__;0vs0.0newly unequal;_v3suffix;All are fixed.
Choices worth a look
/a/b), and messages name nodes that way, while a consolidated key isa/b. zarr-python'screate_hierarchykeys by relative paths, with""for the root. If a future hierarchy model uses that form instead, only the key conversion changes.==: a v2 fill value, the fill value of a data type nothing in scope claims,attributes,extra_fields, and anUnclaimedfield's configuration.==takestruefor1and-0.0for0.0. Comparing that JSON "as written" or "as JSON Schema compares" is a separate decision, so the README says how it compares today..zarrayholdingattributesstays aninvalid_value. The v2 spec lets.zarrayhold other keys (readers ignore them), while.zgroupforbids them, sounknown_keywould be wrong there.input, as the package'sunknown_keyproblems already do. pydantic instead locates dict-key errors at(..., key, "[key]"), with the key as input.Left out
hierarchy_problems.==(choice 2)..zarraymembers;unknown_keyleaves no model;.zmetadataentries are checked only as JSON;consolidated_metadata: nullgoes unrecorded;attributesin.zarrayhides the document's other problems.Stack
Stacked on #379 (
feat/zarr-metadata-validate-at-construction), itself on #378 → #377 → #376 → #374 → #373 → #372 → #371 → #370. Merge after #379. Fragments:381.bugfix.md,381.bugfix.1.md,381.bugfix.2.md,381.bugfix.3.md,381.feature.md,381.feature.1.md. The fragments were numbered for this PR, #381. If the number differs, rename them.🤖 Generated with Claude Code