Skip to main content

dr-serialize

JSON-safe serialization and canonical hashing for Python: two deliberately separate lanes - a policy-driven normalization lane and a strict identity lane - plus general-purpose Canonical JSON Text utilities:

                normalization lane (policy)
Any value --> Serializer.to_jsonable(...) --> diagnostic normalized JSON value
                                                   |
                canonical JSON text (deterministic) v
 canonical_json(...) --> stable text --> canonical_json_bytes(...) --> json_hash(...)

                identity lane (strict, policy-free)
Raw mapping --> IdentityDocument --> canonical_identity_json --> identity_document_hash
  • The normalization lane is policy: it decides what your objects become as a diagnostic normalized JSON value - extensible via handlers, bounded by explicit limits, lossy where it must be.
  • The Canonical JSON Text utilities are deterministic: they encode finite strict JSON data as canonical JSON text, exact UTF-8 bytes, and hashes - no handlers, no limits, same input, same bytes, forever.
  • The identity lane is strict: it validates a strict JSON value and the exact Identity Document shape, then hashes the canonical identity JSON text's UTF-8 bytes - no coercion, and a diagnostic normalized JSON value never feeds it.

The terms and contracts reference renders the authoritative vocabulary in .defs/terms.toml and binding behavioral rules in .defs/contracts.toml, including term categories, relationships, definitions, and mappings to exported names.

Normalization and canonical JSON text generation compose at your call site, so hashes never silently depend on serialization policy:

hash_value = json_hash(serializer.to_jsonable(value))

Ecosystem

dr-serialize provides serialization/schema utilities shared across the dr-* stack: JSON-safe conversion, explicit limits, and canonical hashing. Neighbor repos are dr-providers, dr-graph, dr-platform, dr-code, whetstone-ai, and unitbench. This repo depends directly on pydantic and no named ecosystem neighbor; no consumer repo is declared here, though tests document extraction lineage from whetstone-ai.

Normalization: Serializer

from dr_serialize import Serializer, postgres_jsonb_limits

serializer = Serializer(limits=postgres_jsonb_limits())
payload = serializer.to_jsonable(anything)   # JSON-safe, size- and depth-checked

A Serializer bundles SerializationLimits with an ordered tuple of handlers. Built-in handlers cover scalars, sequences, mappings, bytes, types, Pydantic models, coroutines/generators, and __dict__ objects. Your handlers run after the scalar/container built-ins and before the fallbacks, so they can intercept any non-primitive value:

from dr_serialize import ConversionContext, JsonableHandle, Serializer, postgres_jsonb_limits

def jsonable_point(x: object, ctx: ConversionContext) -> JsonableHandle:
    if isinstance(x, Point):
        return True, {"x": ctx.convert(x.x, "x"), "y": ctx.convert(x.y, "y")}
    return False, None      # fall through to the next handler

serializer = Serializer(limits=postgres_jsonb_limits(), handlers=(jsonable_point,))

Handlers recurse through ctx.convert(child, key) - the library owns depth and path bookkeeping, and the configured max_depth is enforced through handler recursion too.

Limits

SerializationLimits (frozen) carries max_depth, max_bytes, and hard_max_bytes. postgres_jsonb_limits(max_bytes=...) is the shipped preset for Postgres JSONB storage; construct your own for other ceilings. to_jsonable requires limits explicitly - every call site states its storage policy.

Canonical JSON Text: text, bytes, and hashes

from dr_serialize import canonical_json, canonical_json_bytes, json_hash

text = canonical_json(payload)              # profile-v1 deterministic text
data = canonical_json_bytes(payload)        # exact UTF-8 encoding of text
digest = json_hash(payload)                 # validated full Sha256Digest
display = json_hash(payload, length=16)     # ordinary truncated string

All three functions are bound to the dr-serialize Canonical JSON Text profile v1: object keys are sorted, list order is preserved, separators are compact, non-ASCII characters are escaped, and input must be a finite strict JSON value at runtime (including string-only object keys and no reference cycles). This is not RFC 8785. An incompatible profile requires a separately named API; these functions have no profile or version parameter.

Inputs are Jsonable values, typically the output of Serializer.to_jsonable or values you construct yourself. These utilities are intentionally policy-free: hashes are long-lived keys, so they must never change because a handler was added or a limit tuned. If you need conversion first, compose with the normalization lane explicitly.

Deterministic arrays from unordered collections

canonical_json preserves array order by design, so an array built from a set cannot be repaired afterwards. canonical_sorted_values orders already-JSON-safe values by their canonical JSON text instead, projecting a logically unordered collection into one deterministic array. Serializer applies it to every set and frozenset it normalizes; call it directly wherever else an unordered collection becomes JSON - notably in a Pydantic field_serializer, so model_dump(mode="json") is byte-stable:

import pydantic
from dr_serialize import Jsonable, canonical_sorted_values

class Capabilities(pydantic.BaseModel):
    tags: frozenset[str]

    @pydantic.field_serializer("tags")
    def serialize_tags(self, value: frozenset[str]) -> list[Jsonable]:
        return canonical_sorted_values(value)

The order is arbitrary-but-stable lexicographic order over canonical JSON text, not a human collation: 10 sorts before 2 and true sorts after every number. The helper is policy-free - it never normalizes, coerces, or deduplicates, duplicates survive, and the iterable is consumed once - and it must never be used to reorder a semantically ordered array. Members outside strict Jsonable raise JsonEncodeError with the member's input index prefixed onto the error path.

Sha256Digest is the nominal boundary for full SHA-256 values. Its parse() method accepts exactly 64 lowercase hexadecimal characters and raises Sha256DigestError for prefixes, alternate case, whitespace, truncation, or other malformed values. It also provides a strict Pydantic core schema, so TypeAdapter(Sha256Digest) and model fields preserve the nominal type, serialize as JSON strings, and publish the exact length and lowercase-hex pattern in JSON Schema.

Bounded strict JSON decoding

decode_strict_json_bytes consumes one complete UTF-8 JSON value under mandatory byte and structural-depth limits:

from dr_serialize import decode_strict_json_bytes

value = decode_strict_json_bytes(
    frame_bytes,
    max_bytes=len(frame_bytes),
    max_depth=32,
)

The decoder is strict and non-coercing. It rejects byte-order marks, invalid UTF-8, duplicate decoded object keys, non-finite numbers, malformed or trailing input, and multiple root values. Its iterative structural parser enforces max_depth without depending on Python recursion. Typed failures expose only bounded structural metadata, do not echo or retain input, and are safe to persist. This guarantee covers exception messages, diagnostics(), and chained causes/contexts. Traceback locals are the exception: tooling that captures them can retain function arguments, so do not persist traceback locals from secret-bearing decode calls.

Identity lane: Identity Document and identity_document_hash

The identity lane is the strict, policy-free path for cross-repo domain identity: validate a strict JSON value, wrap it in the exact three-field Identity Document {schema, schema_version, payload}, render its Canonical Identity JSON Text, and hash that text's UTF-8 bytes into the full Identity Hash. The terms and contracts reference defines each term; the owning domain chooses the schema, version, and complete payload - dr-serialize only validates.

from dr_serialize import (
    build_identity_document,
    canonical_identity_json_bytes,
    identity_document_hash,
)

doc = build_identity_document(
    schema="example.config",       # the owning domain chooses this
    schema_version=1,              # ...and this
    payload={"identity_field": "value"},  # ...and the complete payload
)
h = identity_document_hash(doc)    # full 64-char lowercase SHA-256 hex
wire = canonical_identity_json_bytes(doc)  # the exact bytes h covers

IdentityDocument owns a private snapshot of its payload. Both its public payload property and to_json_dict() return fresh deep-copied ordinary JSON values, so mutating any caller-visible alias cannot change its canonical bytes or hash.

Rejections are typed: StrictJsonError for values that are not strict JSON values, IdentityDocumentError for documents that are not the exact three-field shape. There is no truncation parameter on this path; identity_hash_prefix is a separate, display-only helper that never establishes identity. A diagnostic normalized JSON value never feeds identity hashing. Committed golden vectors live in tests/fixtures/identity_golden.json for dependent repos to reuse.

Errors

Both lanes and the Canonical JSON Text utilities raise from one typed taxonomy rooted at SerializationError. Outside the strict decoder, diagnostics are structurally persistable and their preview and detail fields are bounded, but they may contain payload-derived data or an underlying exception's repr and are not secret-safe. Every error carries the path to the offending value plus a diagnostics() dict:

Error Raised by
MaxDepthExceededError engine: nesting exceeded max_depth
JsonEncodeError engine probe and canonical JSON text: value not JSON-encodable (canonical requires finite strict JSON input)
PayloadTooLargeError engine: encoded size exceeded max_bytes
ModelDumpError engine: Pydantic model_dump failed
ObjectVarsSerializationError engine: __dict__ walk failed
ValueTransformError base for consumer handler failures - subclass it with a message_prefix
StrictJsonError identity lane: value is not a strict JSON value (non-JSON, non-string key, NaN/inf, cycle)
IdentityDocumentError identity lane: document is not the exact three-field shape
StrictJsonDecodeError base for strict bytes-first decoding failures
JsonByteLimitError decoder: encoded input exceeds max_bytes
JsonDepthLimitError decoder: nesting exceeds max_depth
InvalidUtf8Error decoder: input is not strict UTF-8
JsonSyntaxError decoder: malformed, incomplete, or trailing JSON
DuplicateJsonKeyError decoder: an object repeats a decoded key
NonFiniteJsonNumberError decoder: input contains a non-finite number

Sha256Digest.parse() raises Sha256DigestError, a ValueError, for a malformed full digest.

API surface

Normalization: Serializer, ConversionContext, JsonableHandler, JsonableHandle, SerializationLimits, postgres_jsonb_limits, POSTGRES_JSONB_PAYLOAD_MAX_BYTES, POSTGRES_JSONB_MAX_BYTES. Canonical JSON Text: canonical_json, canonical_json_bytes, canonical_sorted_values, json_hash, Sha256Digest, Sha256DigestError. Strict decoding: decode_strict_json_bytes, StrictJsonDecodeError, JsonByteLimitError, JsonDepthLimitError, InvalidUtf8Error, JsonSyntaxError, DuplicateJsonKeyError, NonFiniteJsonNumberError. Identity lane: validate_strict_json, IdentityDocument, build_identity_document, validate_identity_document, canonical_identity_json, canonical_identity_json_bytes, identity_document_hash, compute_identity_hash, identity_hash_prefix, IDENTITY_DOCUMENT_FIELDS. Boundary type: Jsonable. Errors: the taxonomy above plus JsonPath, preview_repr, detail_repr.

Metadata

Release files for dr-serialize 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dr-serialize 0.1.1
File Size Uploaded
dr_serialize-0.1.1.tar.gz 22.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dr-serialize 0.1.1
File Interpreter ABI Platform
dr_serialize-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 49.2 kB

Release files / dr_serialize-0.1.1.tar.gz

Download URL dr_serialize-0.1.1.tar.gz
Size 22.1 kB
Tags Source
SHA-256 checksum
How to use checksums
4c718347a948023ceba5c1928db868616bd36254ae5d53d587eeb43304960cbd
BLAKE2b-256 checksum
How to use checksums
83ef973bda063b26269fcac29906a83ae02808bcc4a01ad5eb3a970b8ba2adf4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.

Transparency log

Release files / dr_serialize-0.1.1-py3-none-any.whl

Download URL dr_serialize-0.1.1-py3-none-any.whl
Size 27.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3b9569cb6a304cde65b0a6c511a388229366b226b4110886c9fcef07993185ff
BLAKE2b-256 checksum
How to use checksums
133de4e82f30daec9f137fbe9349f66d271e6a919f07613b2e9dcd162f6d7e38
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.2

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page