Repository navigation
Conversation
…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
…t calling repr `shown` relied on `repr` raising RecursionError for a deeply nested value. Python 3.14 limits recursion by the real C stack, so on Linux a list nested 100,000 levels deep printed fine and the problem message was the whole value. Now any value with a problem past `JSON_DEPTH` is reported as "a value nested too deep to show", on every interpreter and platform. The test nests one level past the limit, where repr always works. Assisted-by: ClaudeCode:claude-opus-5-5
…SON values `shown_by_python` caught RecursionError so that a non-JSON value nested deeper than repr could handle was described instead of raising. No parsed document can contain such a value: sets and frozensets are not JSON. Its test only passed where the platform's stack happened to overflow at the chosen depth. Values a document can hold that nest past `JSON_DEPTH` are already reported as too deep by `shown`, without repr. Assisted-by: ClaudeCode:claude-opus-5-5
- `shown` reports "too deep" only for the depth problem itself. A non-JSON value sitting exactly on the last allowed level is now printed instead of being mislabeled. - Complex fill-value rules keep each component problem's `input` and `ctx`; only the `loc` is moved under the component index. - `Schemas.defined` restores every `$defs` entry, name and use count a failed write touched, not only its own name. - The node JSON Schema docstring no longer says `consolidated_metadata` may be `null`. Both the schema and the validator refuse that. - The 382 bugfix changelog fragment is rewritten for users without naming internals. - "modelled" is spelled "modeled" in new text. Assisted-by: ClaudeCode:claude-opus-5-5
d-v-b
force-pushed
the
fix/zarr-metadata-review-fixes
branch
from
October 6, 2026 15:53
9505570 to
96f17e3
Compare
…lish 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 🤖
Readers, validators and parsers now stop at 256 levels of nesting (
JSON_DEPTH) and report a deeper level as aninvalid_valueproblem. Before, a small document nested a thousand levels deep raisedRecursionErrorfrom every entry point, and so didjson.loadson store bytes made of 100,000[(now aninvalid_jsonproblem). 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 one root.Other fixes in this PR
ReadorUnclaimedobject placed inside a document passed to a reader is refused as not JSON. Before, it passed validation and let a model write a document the same validator refused.UNSET, notNone: a reading'sdata_type,chunk_gridandchunk_key_encodingwhen the key is missing, andRefused.jsonwhen the field was not JSON. A field with nonameis amissing_keyproblem.consolidated_metadata: null(written by a zarr-python 3.0.x bug) is aninvalid_typeproblem. It is no longer read as absent, and neither JSON Schema admits it.^[a-z][a-z0-9-_.]+$or be a URI in RFC 3986 characters. Other names are refused before any definition is looked up. A definition built with an invalid name raisesTypeError. A raw-bits name with more than 100 digits is an extension name, not a size.canonicalfunction names its definition.ValidationErrorwith one line error per problem (type, loc, input, ctx) instead of one concatenatedvalue_error.Where it comes from
Six adversarial review passes over the stack #370 → #381: Hypothesis-based verdicts, spec text against code, public API and docs, hostile input on free-threaded 3.14, zarr-python and numpy as oracles, and mutation testing. Fixes to #381's own commits were amended there; this PR carries the rest. Three later commits fix what further review rounds found: the "too deep" message no longer depends on
reproverflowing (which it does not on Python 3.14), a deadRecursionErrorfallback is removed, and six smaller findings are fixed.What changes for a caller
zarr.jsonnested 1,000 deepRecursionErrorfrom every readerinvalid_valueat level 256.zarraynested as deep as the cap admitsfrom_json,to_json,==raiseRecursionErrorconsolidated_metadataRecursionErrorfrom every group readerinvalid_valueat level 256b"[" * 100000atzarr.jsonRecursionErrorfromfrom_key_valueinvalid_jsonReadbuilt by hand inside caller JSONinvalid_typeat the fieldreading.data_typefor a missing keyNoneUNSETRefused.jsonfor a non-JSON fieldNoneUNSET{"configuration": {}}as a fieldinvalid_typeatnamemissing_keyatnameconsolidated_metadata: nullinvalid_type;from_jsonraises; schema refusesdata_type: ""or"foo/bar"Unclaimed, no problemRefused,invalid_valueatnamedata_type: "r" + 4301 digitsValueErrorUnclaimedDefinition(name="Acme", ...)TypeErrorconfiguration["x"] = 1TypeErrornaming the rulezarr_format: 10**5000(Python object)ValueErrorfromshownzarr_formatnested 100,000 deep (Python object)RecursionErrorfromreprValidationErrorfor two problemsvalue_error, messages concatenatedtype,loc,input,ctxVerification
nullrefused, name syntax, depth cap, documents that crashed before and answer now, fields no longer reached past a refusal, a non-JSON v2dtypereported where it sits).Design choices to look at
refine_json, so one cap there protects every walker after it,json.dumpsincluded. 256 leaves about half of Python's default recursion limit to the caller, because the costliest operations (copy.deepcopy, andpicklebefore 3.12) use two frames per level. Real documents nest below 30 levels.Readinside a model is still trusted.dataclasses.replace(model, codecs=(Read(...),))takes the field as read, as feat(zarr-metadata)!: check each model when it is built, and write it as it is #379 documents. Only documents andupdate's members are checked. HavingReadjudge its own configuration is the follow-up feat(zarr-metadata)!: check each model when it is built, and write it as it is #379 named. (Superseded in feat(zarr-metadata)!: make v3 models a pair of document and scope #393, where models can no longer be built from fields.)2**60 + 2**36 + 1.Left out
Readjudging its configuration when built by hand (choice 2; moot after feat(zarr-metadata)!: make v3 models a pair of document and scope #393).vlen-utf8andvlen-bytesdefinitions;is_/parse_node_metadata_v3;validate_consolidated_*.KeyErroron bad consolidated paths, NaN payloads lost,OverflowErroron int fills), to be filed separately.Stack
Stacked on #381 (
fix/zarr-metadata-foreign-document-gaps), itself on #379 → #378 → #377 → #376 → #374 → #373 → #372 → #371 → #370. Merge after #381. Fragments are numbered 382; rename if the number differs.🤖 Generated with Claude Code