Repository navigation
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
d-v-b
added a commit
that referenced
this pull request
Oct 6, 2026
…of the readers This commit fixes what six review passes found, beyond the fixes already in #381. Nesting depth. A reader now walks at most 256 levels (JSON_DEPTH) and reports a deeper level as an invalid_value problem. Before, a small document nested a thousand levels deep made every validator, reader, parser and from_key_value raise RecursionError, and json.loads on store bytes made of 100,000 "[" did the same (now an invalid_json problem). A chain of groups, each holding the next in its consolidated metadata, was also unbounded. Each document is now read from its position in the outer document, so depth is counted from the outer root. is_json and the is_* guards stop at the same depth. A v2 dtype of field records is checked as JSON before its shape is read. Readers, writers and comparisons use one stack frame per level; the v2 models no longer use copy.deepcopy, which used two. validate_json takes the location of its value, so the v2 validator counts depth from the document root as the v3 readers do. arrays_to_tuples uses one frame per level. A problem copied by with_input or prefixed is not re-checked. A struct fill value is judged in linear time. An integer too long to print is shown by its bit length, a value too deeply nested is described instead of printed, and a non-string key is shown with Python's repr. Hand-built field objects. A Read or Unclaimed object placed inside a document passed to a reader is not JSON and is now refused. Before, it passed validation as if it had been read, so a model could write a document that the same validator then refused. Only a model's own fields are treated as already read. Absent values. A reading's data_type, chunk_grid and chunk_key_encoding are UNSET when the document has no such key, and Refused.json is UNSET when the field was not JSON. Both used to be None, which is also what a JSON null becomes. A field with no name is a missing_key problem. A consolidated_metadata of null is an invalid_type problem; it is no longer read as absent, and neither JSON Schema admits it. ZarrV3ConsolidatedMetadata declares must_understand as False. Extension names. An extension name must match ^[a-z][a-z0-9-_.]+$ or be a URI in RFC 3986 characters, so Python's re and ECMA-262 read the schema pattern the same way. Any other name is refused before a definition is looked up; before, any string was read as an unknown extension. well_named checks this, validate_metadata_field_v3 enforces it, the JSON Schema's unclaimed branch carries the pattern, and a definition with an invalid name raises TypeError when built. A raw-bits name with more than 100 digits is treated as an extension name, not a size. Definition functions always receive a read-only view of the configuration, and an error raised by a canonical function names its definition. Pydantic. The pydantic types raise ValidationError with one line error per problem, carrying type, loc, input and ctx. For a missing key the input is the object that lacks it. A message containing a ctx placeholder like {expected} is passed as the ctx's "message" entry, because pydantic renders messages as templates with no escaping. Tests. New tests pin each behavior the mutation review found unpinned: the stricter of two bounds per keyword, the unclaimed envelope in the schema, no empty attributes written for a group, value_at at index 0, every integer type's range, union naming, layered Annotated metadata, and construct's default factory. The float-bits property test draws sign, exponent and mantissa separately so it reaches normal values. A shard nested to the depth cap is read, written, hashed, pickled, deep-copied and walked; one level deeper reports the depth problem. The same holds for a v2 document and a v3 fill value at the cap. Docs: two spec anchors fixed, the struct data type rendered, a note on the JSONValue export, a typed TypeAdapter example, with_problems compared to zod's treeifyError, and the float fill value rounding cited to zarrs and tensorstore. Assisted-by: ClaudeCode:claude-fable-5-1
Owner
Author
|
🤖 AI text below 🤖 The agent folded this PR, with the rest of the stack, into #398, one pull request against main. The commits and fragments are unchanged there. |
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