Skip to content

feat(zarr-metadata): add optional msgspec integration module - #312

Open
d-v-b wants to merge 3 commits into
mainfrom
claude/friendly-proskuriakova-04afaf
Open

feat(zarr-metadata): add optional msgspec integration module#312
d-v-b wants to merge 3 commits into
mainfrom
claude/friendly-proskuriakova-04afaf

Conversation

@d-v-b

@d-v-b d-v-b commented Aug 29, 2026

Copy link
Copy Markdown
Owner

🤖 AI text below 🤖

Adds zarr_metadata.msgspec, the msgspec sibling of zarr_metadata.pydantic: an optional integration module (msgspec stays out of the core package's dependencies) so the frozen-dataclass metadata models can be used as field types in msgspec.Struct classes and with msgspec.json.decode / msgspec.convert.

Design

msgspec consults its extension hooks only for types it does not support natively — and it supports dataclasses natively, so annotating a field with a core model class directly would engage msgspec's own field-by-field coercion and bypass from_json. The module therefore exports:

  • Field types as runtime markers: empty, non-instantiable ABCs that msgspec treats as custom types, with each core model class registered as a virtual subclass — so the instances dec_hook returns pass msgspec's result check and ARE the core classes (equality, isinstance, nesting all interoperate). Static type checkers see each field type as its core model class.
  • dec_hook / make_dec_hook(wrapped=...): raw documents route through from_json (the single source of truth for structural validation and normalization), existing model instances pass through unchanged by identity, and MetadataValidationError surfaces as msgspec.ValidationError carrying the loc-annotated problem messages plus msgspec's field path. make_dec_hook composes with a decode hook an application already has, including custom types with unhashable annotation objects.
  • No enc_hook, deliberately: msgspec's value-driven encoders never consult enc_hook for dataclasses, so no hook can route a model through to_json — encoding a model directly either raises (the UNSET sentinel) or silently emits a non-canonical field dump. Serialization stays explicit via to_json(); the module docstring documents this (plus the one-custom-type-per-union and no-JSON-schema limits), and pinning tests fail loudly if msgspec's behavior ever changes. This mirrors the TypeScript port's Standard Schema interop, which is likewise validation-only.

Contents

  • src/zarr_metadata/msgspec.py — the module, with usage docs
  • tests/model/test_msgspec_module.py — round-trips across all seven field types, instance pass-through, wrapped-hook composition, loc-annotated error surfacing, registry parity, marker non-instantiability, and pins on the msgspec behaviors the design depends on; canonical test documents are shared with the pydantic tests via tests/model/_cases.py
  • msgspec added to the package test dependency group (floor >=0.19, matching the zarr package; suite verified against both 0.19 and 0.21)
  • docs: docs/api/msgspec.md, nav + index mentions, and a towncrier fragment

🤖 Generated with Claude Code

d-v-b added 2 commits August 29, 2026 12:04
zarr_metadata.msgspec is the msgspec sibling of zarr_metadata.pydantic:
field types over the core metadata models plus a dec_hook (and a
make_dec_hook composer for applications with hooks of their own) so the
models can be used as field types in msgspec Structs and with
msgspec.json.decode / msgspec.convert.

msgspec consults its hooks only for types it does not support natively,
and it supports dataclasses natively — its field-by-field coercion would
bypass from_json — so each field type is a runtime marker class msgspec
treats as a custom type, with the core model class registered as a
virtual subclass; decoded instances ARE the core classes, and static
type checkers see the field types as the core classes. Raw documents
route through from_json, existing instances pass through unchanged, and
MetadataValidationError surfaces as msgspec.ValidationError with the
loc-annotated problem messages. Serialization cannot be delegated
(msgspec's value-driven encoders never consult enc_hook for
dataclasses), so the canonical path stays an explicit to_json();
the module documents this and a test pins the msgspec behavior.

Assisted-by: ClaudeCode:claude-fable-5
Findings from a multi-angle review of the new module, applied:

- Import msgspec at module scope so the integration fails fast where
  msgspec is absent, mirroring zarr_metadata.pydantic; the docstring's
  "requires msgspec" claim was previously unenforced.
- Treat unhashable annotation objects as ordinary lookup misses: they
  previously raised TypeError past the KeyError guard, breaking the
  NotImplementedError contract and preventing a composed hook from ever
  delegating to the wrapped hook.
- Make the runtime markers non-instantiable (shared _FieldType base):
  a hollow marker instance would have passed msgspec's result check
  while having none of the core class's fields. The module docstring
  now also states the exported names are for annotations only.
- Derive the marker registrations from the decode table and add a
  parity test, so a marker cannot pair with one core class in the
  registration and another in the decoder, and the table provably
  covers the exported surface.
- Document the third msgspec limit (no JSON Schema for custom types
  without a schema_hook) and show the explicit to_json serialization
  path in the docs index snippet.
- Share the canonical test documents between the pydantic and msgspec
  integration tests via tests/model/_cases.py instead of a verbatim
  copy, correct the no-mutable-state test to claim only what from_json
  guarantees, and pin the native-encode test to msgspec's message
  rather than the sentinel type name.

Assisted-by: ClaudeCode:claude-fable-5
ci/check_changelog_entries.py requires an integer PR-number stem, which
the towncrier orphan name used until the PR existed.

Assisted-by: ClaudeCode:claude-fable-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant