Changelog

0.15.0 (2026-10-02)

  • Breaking: the validation callable of Constraint is now positional-only and stored privately as _fn. Constraint(fn=...) raises TypeError: Constraint takes no keyword arguments (it used to be accepted), the non-callable message is now _fn must be callable, the callable is read as constraint._fn rather than constraint.fn, and repr() / __rich_repr__() report _fn=. Factory(factory=...) is rejected the same way. Subclass constructors (NumericConstraint(gt=0), StrConstraint(...), TimezoneConstraint(tz=True), …) are unaffected.

  • Hashing a self-referential frozen Struct, and struct_check_types() on a struct that contains itself, now raise RecursionError instead of overflowing the C stack. A shared but acyclic reference still passes.

  • Fix undefined behavior when decoding timedelta: the integral part was applied with a uint64 multiply that wrapped for very large day counts, silently producing a wrong (sometimes negative) duration. Out-of-range input now raises ValidationError.

  • datetime decoding no longer relies on undefined double to int64 conversion: out-of-range float timestamps raise ValidationError: Timestamp is out of range, and a 9999-12-31T23:59:59.999999 timestamp whose microsecond rounding pushes past the maximum epoch is rejected instead of wrapping.

  • Fix signed overflow when negating -2**63: parsing -9223372036854775808 from JSON, and checking min / max constraints against it, now behave correctly.

  • Non-finite Decimal values (NaN, Infinity) now encode as null under struct_dump_json(decimal_as_number=True), matching the float codec, instead of emitting invalid JSON. The default decimal_as_number=False is unchanged.

  • struct_dump_csv() now raises TypeError: CSV supports flat scalar fields only for an Enum member whose value is a list, tuple, dict, or set, instead of writing a bogus cell. A tag mismatch on a non-str cell now reports a proper ValidationError.

  • struct_dump() and struct_validate() handle their boolean keyword arguments through the same path as the JSON codec. An object whose __bool__ raises now propagates that exception instead of being treated as True with an exception left set.

  • StructAdapter now validates constraint / type compatibility eagerly at construction, matching Struct class creation, including through NewType and PEP 695 type aliases.

  • External struct types — anything exposing __struct_fields__ / __struct_defaults__ — may expose those as any iterable of str, not only a tuple. A non-iterable, or a non-str field name, raises TypeError.

  • str subclasses are now accepted as JSON input.

  • Dumping a set subclass whose iterator yields more items than PySet_GET_SIZE reported no longer reads past the end of the list.

  • Fix generated JSON schemas dropping Field metadata (title, description, examples, deprecated) on tagged-union member $refs.

  • Fix integer tags in a tagged union’s discriminator.mapping being emitted as numbers instead of strings.

  • Field(examples=...) is deep-copied into the generated schema, so mutating a returned schema no longer mutates the Field.

  • Fix the type stub: UnsetType and the internal NODEFAULT sentinel type are no longer declared as enum.Enum subclasses (they are singletons at runtime), and Factory / Constraint are declared positional-only to match the runtime.

  • Add Struct.__slots__ and the order=True comparison dunders (__lt__, __le__, __gt__, __ge__) to the type stub.

  • Free-threaded builds: dropped the container critical sections taken while encoding, dumping, or validating user dicts. structtype’s own mutable state (lazily-built *Info objects, caches, the hash cache) stays thread-safe, but mutating a container from one thread while another thread is processing it is no longer guarded. Removing the locks avoids serialising independent operations and any lock-order hazards.

  • struct_config is now always present on every struct type and instance, defaulting to {} when no class in the hierarchy declares one (it previously raised AttributeError on classes that never set it, despite the type stub declaring it). Matches pydantic’s model_config behavior.

  • Fix the type stub: remove the phantom StructMeta.struct_config property that never existed at runtime.

  • Struct creation now emits a UserWarning when a new field’s name starts with struct_ and matches a known struct member — one of the struct_* methods or struct_config. Shadowing remains allowed (the field still wins); the warning reports it at class creation instead of a later TypeError: 'int' object is not callable. Shadowing anything else (parent methods, dunders), struct_-prefixed names that match no known member, and re-declaring an inherited field stay silent, as in plain Python. Use warnings.filterwarnings("error", ...) to escalate the warning to a hard error.

0.14.0 (2026-09-19)

  • Field(json_schema_extra=...) is now applied after the type’s generated JSON schema, so it can override generated keys such as type and format (matching pydantic). Its values replace list properties instead of being concatenated with them.

  • Add native support for pathlib.PurePath and all of its subclasses (pathlib.Path, pathlib.PurePosixPath, pathlib.PureWindowsPath, pathlib.PosixPath, pathlib.WindowsPath). Path values decode to the annotated class (the platform flavour for pathlib.Path and pathlib.PurePath), are serialized through __fspath__, and validation is strict per annotation (a pathlib.Path field rejects a pathlib.PurePosixPath).

  • Add native support for the full ipaddress family: IPv4Address, IPv6Address, IPv4Network, IPv6Network, IPv4Interface, and IPv6Interface, plus user subclasses. They serialize to strings in JSON and accept string input for validation, are preserved by builtin_types / ALL_BUILTIN_TYPES, decode to the annotated class (networks are constructed strictly), and map to the ipv4, ipv6, ipv4network, ipv6network, ipv4interface, and ipv6interface JSON schema formats.

  • Performance: accelerate JSON string encoding and decoding with SIMD scanning.

  • Fix a possible crash during CSV validation when recursive type initialization leaves field type information incomplete; raise RuntimeError instead.

0.13.0 (2026-09-15)

  • Fix minItems in generated JSON schemas for array_like=True Structs whose fields are all optional: it was one too high (an untagged Struct reported minItems: 1 for an empty-array payload that decoding accepts; a tagged one reported 2 for a tag-only payload). Matches the fix in msgspec PR #1124.

  • Preserve Field metadata and StrConstraint on annotated dictionary keys in generated JSON schemas: they now appear under propertyNames instead of being dropped. Matches msgspec PR #1159.

  • Report out-of-range min_length / max_length on StrConstraint, CollectionConstraint, and BytesConstraint as an “out of range” ValueError instead of a misleading “must be >= 0” message. Matches msgspec PR #1172.

  • Fix a SystemError when coercing an int too large for a C double into a float field; struct_validate / StructAdapter now raise ValidationError: Number out of range. Matches msgspec PR #1162.

  • Fix inherited field alias / rename not being reset when a subclass re-declares the field back to its own name. Matches msgspec PR #1133.

  • Fix NameError when defining a Struct with an unquoted forward reference on Python 3.14+ (PEP 649 deferred annotations). Matches msgspec PR #1168.

  • Add Struct.struct_validate_csv() and Struct.struct_dump_csv() for CSV encoding/decoding. These operate on the standard library’s csv module: struct_validate_csv(row) decodes one row (a sequence of cell strings) into a Struct, and struct_dump_csv() returns one row as a list[str] for the caller to pass to csv.writer.writerow(). The caller owns the delimiter, quoting, dialect, encoding, and stream; structtype does no buffer/bytes/ encoding handling. CSV is positional — each cell is matched to a field by its position in __struct_fields__ declaration order, with no header row and no alias / rename support. Decoding is always lax, and only flat scalar fields are supported (nested Struct values and list / tuple / dict / set cells raise ValidationError on decode and TypeError on dump). Encoding always returns every field positionally, so omit_defaults does not apply. For array_like=True, a configured tag is written as the first cell and validated on decode, matching the JSON codec; other structs ignore tag. null_values is the only structtype option; bytes are dumped as base64, None as an empty cell, bool as true/false, enum members as their value, and the datetime family, uuid.UUID, and decimal.Decimal as their standard string forms. Serializer codecs and Constraint validators on flat scalar fields are honored (load and constraints on decode, dump on encode).

0.12.0 (2026-09-11)

  • Fix concurrent unsorted dictionary JSON encoding on free-threaded Python so an empty snapshot produces valid {} output instead of corrupting the surrounding JSON buffer.

  • Allow Serializer annotations on most native types. Previously, Serializer could only be used on custom (user-defined) types. Now it is accepted on bytes, bytearray, memoryview, the datetime family, uuid.UUID, decimal.Decimal, enums, set, frozenset, Struct subclasses, dataclass / attrs types, and any of those made optional with Annotated[T, Serializer(...)] | None (where None bypasses the codec). The blocked set is reduced to: Any, bool, int, float, str, list, dict, tuple, TypedDict, NamedTuple, frozendict, and Literal. Empty Serializer() annotations are rejected on the same blocked types; they remain inert only on supported types.

  • Breaking: Serializer and Constraint annotations must be attached to a concrete type inside Annotated; attaching either directly to a union or an optional type raises TypeError at class creation. Previously accepted forms such as Annotated[int | None, Serializer(...)] and Annotated[Union[A, B], Constraint(...)] are now rejected. Make a field optional by unioning the annotated member with None: Annotated[T, Serializer(...)] | None (likewise for Constraint).

  • StructAdapter now rejects a Constraint attached to a union or optional type at construction, matching Struct’s class-creation behavior. Constraints remain supported on concrete types; Serializer remains unsupported on StructAdapter.

  • Fix Serializer correctness for already-valid enum instances and bytes subclasses, and preserve same-runtime-type results returned by dump across Python and JSON values and dictionary keys.

0.11.0 (2026-09-06)

  • Breaking: remove structtype.Raw. Raw was the only type that did not participate in the full API surface (e.g. it raised a TypeError when passed to struct_dump), and its lazy-decode / verbatim-encode semantics required special-casing throughout the encoder, decoder, and validator. For plain arbitrary-JSON-data fields, Any covers the use case (values decode to plain Python types and re-encode on output). For a dedicated named type, use a wrapper type with a Serializer codec, which works with both the JSON and Python paths. Migration:

    Old

    New

    r: Raw field

    r: Any

    Raw(b'{"x": 1}') verbatim passthrough

    typed wrapper + Annotated[Wrapper, Serializer(dump=..., load=...)] — decoded on input, re-encoded on output

  • Improve musllinux wheels build.

  • Internal performance improvements.

0.10.0 (2026-09-04)

  • Breaking: remove uuid_format and add uuid_as_hex. The uuid_format keyword argument ("canonical", "hex") is removed from struct_dump_json on both Struct and StructAdapter; passing it now raises a TypeError. It is replaced by uuid_as_hex: bool = False: False (default) encodes uuid.UUID values as canonical strings (with hyphens, same as str(uuid)), True encodes them as hex strings (same as uuid.hex). Decoding still accepts both forms. Hard break with no deprecation shims.

    Migration:

    Old

    New

    obj.struct_dump_json(uuid_format="hex")

    obj.struct_dump_json(uuid_as_hex=True)

    obj.struct_dump_json(uuid_format="canonical")

    obj.struct_dump_json() (default)

  • Breaking: remove decimal_format and add decimal_as_number. The decimal_format keyword argument ("string", "number", or a callable) is removed from struct_dump_json on both Struct and StructAdapter; passing it now raises a TypeError. It is replaced by decimal_as_number: bool = False: False (default) encodes decimal.Decimal values as JSON strings, True encodes them as JSON numbers. Callable support is removed entirely — a callable emitting anything other than a string or number could never be decoded back into a Decimal-typed field (only JSON strings and numbers are parsed as Decimal), so the custom-shaping use case had no working round trip. Custom shaping now requires a wrapper type with a struct_dump / struct_validate protocol method. Hard break with no deprecation shims.

    Migration:

    Old

    New

    obj.struct_dump_json(decimal_format="number")

    obj.struct_dump_json(decimal_as_number=True)

    obj.struct_dump_json(decimal_format="string")

    obj.struct_dump_json() (default)

    obj.struct_dump_json(decimal_format=fn)

    no direct equivalent — encode as str/number or use a wrapper type

  • Breaking: struct_config now returns exactly what the user specified in the class body (the sparse StructConfig dict), not the fully-resolved config. __struct_config__ continues to return the fully-resolved dict with all 15 keys and defaults applied.

    Previously struct_config and __struct_config__ were aliases — both returned the fully-resolved dict. Now they serve different purposes:

    Accessor

    Content

    cls.struct_config

    Original sparse dict (what user wrote)

    cls.__struct_config__

    Fully-resolved dict (all 15 keys, defaults + inherited)

    Migration:

    Old

    New

    cls.struct_config["frozen"] (inherited)

    cls.__struct_config__["frozen"]

    cls.struct_config (fully resolved)

    cls.__struct_config__

    Example:

    class Base(Struct):
        struct_config = StructConfig(frozen=True, tag="base")
    
    class Child(Base):
        struct_config = StructConfig(eq=False)
    
    # Before: both returned {'frozen': True, 'tag': 'base', 'eq': False, ...}
    # After:
    Child.struct_config         # {'eq': False}
    Child.__struct_config__     # {'frozen': True, 'tag': 'base', 'eq': False, ...}
    
  • Performance: enum dump improvement.

  • Performance: string interning improvement for field names during encoding/decoding.

  • Fix JSON schema generation for edge cases.

  • Document StructAdapter(Any) as a generic JSON codec in the usage guide.

  • Internal improvement, cleanup, more tests.

0.9.0 (2026-08-24)

  • Breaking: rename the Validator annotation family to Constraint: Validator → Constraint, NumericValidator → NumericConstraint, StrValidator → StrConstraint, BytesValidator → BytesConstraint, CollectionValidator → CollectionConstraint, and TimezoneValidator → TimezoneConstraint. The new name matches how these mostly behave — declarative constraints on field values (ge, pattern, min_length, …) — and aligns with the internal/JSON-Schema vocabulary. Behavior, constructor parameters, and the one-constraint-per-annotation rule are unchanged. Hard break with no deprecation shims.

    Migration:

    Old

    New

    Validator(f)

    Constraint(f)

    NumericValidator(gt=0)

    NumericConstraint(gt=0)

    StrValidator(pattern="...")

    StrConstraint(pattern="...")

    BytesValidator(min_length=1)

    BytesConstraint(min_length=1)

    CollectionValidator(max_length=3)

    CollectionConstraint(max_length=3)

    TimezoneValidator(tz=True)

    TimezoneConstraint(tz=True)

    Custom validators now subclass Constraint (or a fast subclass) instead of Validator.

  • Breaking: rename struct_validate_self() to struct_check_types() and the validate_on_init config option to check_types_on_init. The new names signal how these differ from struct_validate / struct_validate_json: they are pure type-checks on already-existing values, performing no conversion. Hard break with no deprecation shims.

    Migration:

    Old

    New

    p.struct_validate_self()

    p.struct_check_types()

    StructConfig(validate_on_init=True)

    StructConfig(check_types_on_init=True)

  • Fix: struct_check_types() and check_types_on_init=True now recursively validate nested Struct instances inside containers (list, dict, tuple, set, frozenset). Previously only direct nested Struct fields were checked; Structs nested inside containers were trusted as-is.

0.8.1 (2026-08-23)

  • StructMeta.__new__ now forwards class-statement kwargs (e.g. frozen=True) to __init_subclass__. A user-defined __init_subclass__(cls, **kwargs) on a Struct base receives them unmodified, but they do not configure the struct: configuration comes only from the class-body struct_config attribute. A plain Struct subclass with unknown class-statement kwargs now raises TypeError (previously silently ignored).

    Custom metaclasses that subclass StructMeta can still intercept and consume kwargs in their own __new__ before calling super().__new__.

0.8.0 (2026-08-23)

  • Breaking: replace the StructMeta keyword-argument config API with a pydantic-style class-body struct_config attribute. StructConfig is now a TypedDict (plain dict at runtime). The metaclass validates that struct_config is a dict, rejects unknown keys (catches typos), and requires strict bool values for the 12 boolean options.

    StructMeta.__new__ is now a catch-all: class-statement kwargs (e.g. frozen=True) are silently ignored. Custom metaclasses that subclass StructMeta can intercept and consume kwargs in their own __new__ before calling super().__new__.

    Migration:

    Old

    New

    class Point(Struct, frozen=True)

    class Point(Struct):
        struct_config = StructConfig(frozen=True)

    Point.__struct_config__.frozen

    Point.__struct_config__["frozen"]

    New public API:

    • StructConfig is a TypedDict(total=False) with 15 optional keys (frozen, eq, order, kw_only, repr_omit_defaults, array_like, omit_defaults, forbid_unknown_fields, validate_on_init, weakref, dict, cache_hash, tag, tag_field, rename).

    • struct_config attribute on Struct types (resolved, fully-populated dict).

    • kw_only and rename now exposed on __struct_config__ (previously hidden).

    Key-absence = inherit from base; {"tag": None} explicitly clears an inherited tag; StructConfig() (empty dict) is a no-op override.

    Removed:

    • StructConfig C object type (~500 lines of C deleted).

    • StructConfig.replace() method (use dict merge {**cfg, ...} instead).

    • UNSET sentinel for config options (still used for field values).

    • All class-definition keyword arguments on StructMeta.__new__.

    Simplified: project-wide config defaults no longer require a custom metaclass — a base class with struct_config suffices.

    Note: isinstance(x, StructConfig) raises TypeError on all Python versions (TypedDicts do not support instance checks). Use isinstance(x, dict) instead.

  • Fix: from __future__ import annotations is now fully supported. Lazy string annotations are resolved at class creation, so Serializer(dump=...) codecs, Field(alias=...) metadata, and Validator constraints work exactly as with eager annotations. Previously Serializer(dump=...) codecs and Field(alias=...) were silently dropped under lazy annotations.

0.7.0 (2026-08-22)

  • Breaking: Split Field into three focused annotation types: Field (identity + schema metadata), Serializer (value conversion), and Validator (validation constraints). This is a hard break with no deprecation shims.

    Migration table:

    Old

    New

    Field(gt=...)

    NumericValidator(gt=...)

    Field(ge=...)

    NumericValidator(ge=...)

    Field(lt=...)

    NumericValidator(lt=...)

    Field(le=...)

    NumericValidator(le=...)

    Field(multiple_of=...)

    NumericValidator(multiple_of=...)

    Field(pattern=...)

    StrValidator(pattern=...)

    Field(min_length=...)

    StrValidator(min_length=...) / BytesValidator(min_length=...) / CollectionValidator(min_length=...)

    Field(max_length=...)

    same as min_length

    Field(tz=...)

    TimezoneValidator(tz=...)

    Field(validate=f)

    Serializer(load=f)

    Field(dump=g)

    Serializer(dump=g)

    Field(alias=...), Field(title=...), Field(description=...), etc.

    Unchanged — still Field(...)

    New public API:

    • Serializer(*, load=None, dump=None) — value conversion codecs.

    • Validator(fn=None) — base class for validation; callable with a value.

    • NumericValidator(*, gt=None, ge=None, lt=None, le=None, multiple_of=None) — numeric constraints.

    • StrValidator(*, pattern=None, min_length=None, max_length=None) — string constraints.

    • BytesValidator(*, min_length=None, max_length=None) — bytes constraints.

    • CollectionValidator(*, min_length=None, max_length=None) — list/set/frozenset/tuple/dict constraints.

    • TimezoneValidator(*, tz=True) — datetime/timezone constraints.

    Composition: at most one Field, one Serializer, and one Validator per annotation position; cross-kind combinations encouraged. Passing constraint params to Field now raises TypeError.

  • Breaking: struct_validate_self() and validate_on_init=True are now pure type-checks. If a custom-typed field’s value is not already an instance of the declared type, a ValidationError is raised immediately — Serializer.load and protocol fallbacks (struct_validate/model_validate) methods are not called. Any annotated Validator is still invoked on correctly-typed values. Previously, load was called and its converted result silently discarded; this was wasteful and conflated conversion with validation.

0.6.0 (2026-08-21)

  • Breaking: rename __struct_encode_fields__ to __struct_alias_fields__ for consistency with the alias terminology. Internal C identifiers are also renamed (e.g. struct_encode_fields → struct_alias_fields).

  • Breaking: rename FieldInfo.encode_name to FieldInfo.alias.

  • Breaking: remove StrAdapter. For custom types constructed from a single string argument (IPv4Address, HttpUrl, …), use Annotated[T, Field(dump=str, validate=T)] codecs instead.

  • Breaking: remove the gc Struct configuration option. All Struct types now always support cyclic garbage collection, while retaining deferred tracking for scalar-only instances.

  • Harden JSON serialization buffer sizing against integer overflow, preventing crashes or out-of-bounds writes when processing oversized values.

0.5.0 (2026-08-15)

  • Breaking: remove the enc_hook / dec_hook keyword arguments from struct_dump, struct_dump_json, struct_validate, and struct_validate_json on both Struct and StructAdapter. Custom types now implement the struct_dump / struct_validate protocol methods (pydantic’s model_dump / model_validate are also recognized), or use the new per-field codecs. Passing these arguments now raises a TypeError.

  • Add Field(dump=...) and Field(validate=...) codecs, used as Annotated[X, Field(dump=..., validate=...)]. Codecs may only be attached to custom types; attaching one to a natively supported type or to a union raises a TypeError at class creation time, as does attaching two conflicting dump codecs within a single field. Codecs are supported on Struct fields only — StructAdapter rejects codec’d annotations (use a struct_dump / struct_validate protocol method there).

  • Remove Field(default=...) and Field(default_factory=...). Defaults are now always specified on the class body: a constant via x: int = 3, and a per-instance default by wrapping a callable in the new structtype.Factory, e.g. x: list = Factory(list). Field is now a pure constraints/metadata object and may only be used inside typing.Annotated.

  • Add structtype.Factory(func), a wrapper for per-instance default values. = Factory(list) is equivalent to the previous Field(default_factory=list).

  • Add Field(deprecated=True) to mark a field as deprecated in the generated JSON Schema.

0.4.0 (2026-08-09)

  • Remove struct_force_setattr. To set fields on a frozen struct inside __post_init__, use object.__setattr__(self, ...) (requires Python 3.13+).

  • Replace the encoder order= argument with sort_keys: bool on struct_dump, struct_dump_json. sort_keys=True sorts dict keys and set elements for deterministic output; struct, dataclass, and object fields keep their declaration order. The previous order='sorted' mode is removed.

  • Add tuple with all builtin types usable in struct_dump.

  • Improved validation error messages.

  • Fix possible memory leak.

  • Improve free threading support.

  • Add more safeguards to the C core.

  • Improve FieldInfo repr.

0.3.0 (2026-08-02)

  • Improve C compilation, fix warnings

  • Add Windows wheel builds.

  • Add Python 3.15 wheel builds.

  • Add pydantic json schema support.

  • Internal renames and fix StructAdapter struct_dump.

  • Pass through struct_dump parameters.

0.2.0 (2026-07-27)

  • Improve and cleanup documentation.

  • Cleanup interface of Struct to be minimal and more pythonic.

  • Remove jsonln support on Struct.

  • Add support for types with ducktyping Structs struct_validate and struct_dump.

  • Add StrAdapter, allow str like types to be handled.

  • Add support for iter on Struct, supports dict(Struct).

  • Remove struct_as_dict and struct_as_tuple.

0.1.0 (2026-07-26)

  • First release cut down from latest msgspec release.

  • Goal is to have a Struct datatype with all needed methods on the class itself. Only minimal helper functions around.

  • Support validating and dumping to Python objects.

  • Support validating and dumping to JSON.

  • Interface is beta and could be changed.