Changelog¶
0.15.0 (2026-10-02)¶
Breaking: the validation callable of
Constraintis now positional-only and stored privately as_fn.Constraint(fn=...)raisesTypeError: Constraint takes no keyword arguments(it used to be accepted), the non-callable message is now_fn must be callable, the callable is read asconstraint._fnrather thanconstraint.fn, andrepr()/__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, andstruct_check_types()on a struct that contains itself, now raiseRecursionErrorinstead of overflowing the C stack. A shared but acyclic reference still passes.Fix undefined behavior when decoding
timedelta: the integral part was applied with auint64multiply that wrapped for very large day counts, silently producing a wrong (sometimes negative) duration. Out-of-range input now raisesValidationError.datetimedecoding no longer relies on undefineddoubletoint64conversion: out-of-range float timestamps raiseValidationError: Timestamp is out of range, and a9999-12-31T23:59:59.999999timestamp whose microsecond rounding pushes past the maximum epoch is rejected instead of wrapping.Fix signed overflow when negating
-2**63: parsing-9223372036854775808from JSON, and checkingmin/maxconstraints against it, now behave correctly.Non-finite
Decimalvalues (NaN,Infinity) now encode asnullunderstruct_dump_json(decimal_as_number=True), matching the float codec, instead of emitting invalid JSON. The defaultdecimal_as_number=Falseis unchanged.struct_dump_csv()now raisesTypeError: CSV supports flat scalar fields onlyfor anEnummember whose value is a list, tuple, dict, or set, instead of writing a bogus cell. A tag mismatch on a non-strcell now reports a properValidationError.struct_dump()andstruct_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 asTruewith an exception left set.StructAdapternow validates constraint / type compatibility eagerly at construction, matchingStructclass creation, including throughNewTypeand PEP 695 type aliases.External struct types — anything exposing
__struct_fields__/__struct_defaults__— may expose those as any iterable ofstr, not only a tuple. A non-iterable, or a non-strfield name, raisesTypeError.strsubclasses are now accepted as JSON input.Dumping a
setsubclass whose iterator yields more items thanPySet_GET_SIZEreported no longer reads past the end of the list.Fix generated JSON schemas dropping
Fieldmetadata (title,description,examples,deprecated) on tagged-union member$refs.Fix integer tags in a tagged union’s
discriminator.mappingbeing emitted as numbers instead of strings.Field(examples=...)is deep-copied into the generated schema, so mutating a returned schema no longer mutates theField.Fix the type stub:
UnsetTypeand the internalNODEFAULTsentinel type are no longer declared asenum.Enumsubclasses (they are singletons at runtime), andFactory/Constraintare declared positional-only to match the runtime.Add
Struct.__slots__and theorder=Truecomparison 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
*Infoobjects, 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_configis now always present on every struct type and instance, defaulting to{}when no class in the hierarchy declares one (it previously raisedAttributeErroron classes that never set it, despite the type stub declaring it). Matches pydantic’smodel_configbehavior.Fix the type stub: remove the phantom
StructMeta.struct_configproperty that never existed at runtime.Struct creation now emits a
UserWarningwhen a new field’s name starts withstruct_and matches a known struct member — one of thestruct_*methods orstruct_config. Shadowing remains allowed (the field still wins); the warning reports it at class creation instead of a laterTypeError: '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. Usewarnings.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 astypeandformat(matching pydantic). Its values replace list properties instead of being concatenated with them.Add native support for
pathlib.PurePathand all of its subclasses (pathlib.Path,pathlib.PurePosixPath,pathlib.PureWindowsPath,pathlib.PosixPath,pathlib.WindowsPath). Path values decode to the annotated class (the platform flavour forpathlib.Pathandpathlib.PurePath), are serialized through__fspath__, and validation is strict per annotation (apathlib.Pathfield rejects apathlib.PurePosixPath).Add native support for the full
ipaddressfamily:IPv4Address,IPv6Address,IPv4Network,IPv6Network,IPv4Interface, andIPv6Interface, plus user subclasses. They serialize to strings in JSON and accept string input for validation, are preserved bybuiltin_types/ALL_BUILTIN_TYPES, decode to the annotated class (networks are constructed strictly), and map to theipv4,ipv6,ipv4network,ipv6network,ipv4interface, andipv6interfaceJSON schemaformats.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
RuntimeErrorinstead.
0.13.0 (2026-09-15)¶
Fix
minItemsin generated JSON schemas forarray_like=TrueStructs whose fields are all optional: it was one too high (an untagged Struct reportedminItems: 1for an empty-array payload that decoding accepts; a tagged one reported2for a tag-only payload). Matches the fix in msgspec PR #1124.Preserve
Fieldmetadata andStrConstrainton annotated dictionary keys in generated JSON schemas: they now appear underpropertyNamesinstead of being dropped. Matches msgspec PR #1159.Report out-of-range
min_length/max_lengthonStrConstraint,CollectionConstraint, andBytesConstraintas an “out of range”ValueErrorinstead of a misleading “must be >= 0” message. Matches msgspec PR #1172.Fix a
SystemErrorwhen coercing aninttoo large for a C double into afloatfield;struct_validate/StructAdapternow raiseValidationError: Number out of range. Matches msgspec PR #1162.Fix inherited field
alias/renamenot being reset when a subclass re-declares the field back to its own name. Matches msgspec PR #1133.Fix
NameErrorwhen defining aStructwith an unquoted forward reference on Python 3.14+ (PEP 649 deferred annotations). Matches msgspec PR #1168.Add
Struct.struct_validate_csv()andStruct.struct_dump_csv()for CSV encoding/decoding. These operate on the standard library’scsvmodule:struct_validate_csv(row)decodes one row (a sequence of cell strings) into a Struct, andstruct_dump_csv()returns one row as alist[str]for the caller to pass tocsv.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 noalias/renamesupport. Decoding is always lax, and only flat scalar fields are supported (nestedStructvalues andlist/tuple/dict/setcells raiseValidationErroron decode andTypeErroron dump). Encoding always returns every field positionally, soomit_defaultsdoes not apply. Forarray_like=True, a configuredtagis written as the first cell and validated on decode, matching the JSON codec; other structs ignoretag.null_valuesis the only structtype option;bytesare dumped as base64,Noneas an empty cell,boolastrue/false, enum members as their value, and thedatetimefamily,uuid.UUID, anddecimal.Decimalas their standard string forms.Serializercodecs andConstraintvalidators 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
Serializerannotations on most native types. Previously,Serializercould only be used on custom (user-defined) types. Now it is accepted onbytes,bytearray,memoryview, thedatetimefamily,uuid.UUID,decimal.Decimal, enums,set,frozenset,Structsubclasses,dataclass/attrstypes, and any of those made optional withAnnotated[T, Serializer(...)] | None(whereNonebypasses the codec). The blocked set is reduced to:Any,bool,int,float,str,list,dict,tuple,TypedDict,NamedTuple,frozendict, andLiteral. EmptySerializer()annotations are rejected on the same blocked types; they remain inert only on supported types.Breaking:
SerializerandConstraintannotations must be attached to a concrete type insideAnnotated; attaching either directly to a union or an optional type raisesTypeErrorat class creation. Previously accepted forms such asAnnotated[int | None, Serializer(...)]andAnnotated[Union[A, B], Constraint(...)]are now rejected. Make a field optional by unioning the annotated member withNone:Annotated[T, Serializer(...)] | None(likewise forConstraint).StructAdapternow rejects aConstraintattached to a union or optional type at construction, matchingStruct’s class-creation behavior. Constraints remain supported on concrete types;Serializerremains unsupported onStructAdapter.Fix Serializer correctness for already-valid enum instances and bytes subclasses, and preserve same-runtime-type results returned by
dumpacross Python and JSON values and dictionary keys.
0.11.0 (2026-09-06)¶
Breaking: remove
structtype.Raw.Rawwas the only type that did not participate in the full API surface (e.g. it raised aTypeErrorwhen passed tostruct_dump), and its lazy-decode / verbatim-encode semantics required special-casing throughout the encoder, decoder, and validator. For plain arbitrary-JSON-data fields,Anycovers the use case (values decode to plain Python types and re-encode on output). For a dedicated named type, use a wrapper type with aSerializercodec, which works with both the JSON and Python paths. Migration:Old
New
r: Rawfieldr: AnyRaw(b'{"x": 1}')verbatim passthroughtyped wrapper +
Annotated[Wrapper, Serializer(dump=..., load=...)]— decoded on input, re-encoded on outputImprove musllinux wheels build.
Internal performance improvements.
0.10.0 (2026-09-04)¶
Breaking: remove
uuid_formatand adduuid_as_hex. Theuuid_formatkeyword argument ("canonical","hex") is removed fromstruct_dump_jsonon bothStructandStructAdapter; passing it now raises aTypeError. It is replaced byuuid_as_hex: bool = False:False(default) encodesuuid.UUIDvalues as canonical strings (with hyphens, same asstr(uuid)),Trueencodes them as hex strings (same asuuid.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_formatand adddecimal_as_number. Thedecimal_formatkeyword argument ("string","number", or a callable) is removed fromstruct_dump_jsonon bothStructandStructAdapter; passing it now raises aTypeError. It is replaced bydecimal_as_number: bool = False:False(default) encodesdecimal.Decimalvalues as JSON strings,Trueencodes 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 aDecimal-typed field (only JSON strings and numbers are parsed asDecimal), so the custom-shaping use case had no working round trip. Custom shaping now requires a wrapper type with astruct_dump/struct_validateprotocol 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_confignow returns exactly what the user specified in the class body (the sparseStructConfigdict), not the fully-resolved config.__struct_config__continues to return the fully-resolved dict with all 15 keys and defaults applied.Previously
struct_configand__struct_config__were aliases — both returned the fully-resolved dict. Now they serve different purposes:Accessor
Content
cls.struct_configOriginal 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
Validatorannotation family toConstraint:Validator→Constraint,NumericValidator→NumericConstraint,StrValidator→StrConstraint,BytesValidator→BytesConstraint,CollectionValidator→CollectionConstraint, andTimezoneValidator→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 ofValidator.Breaking: rename
struct_validate_self()tostruct_check_types()and thevalidate_on_initconfig option tocheck_types_on_init. The new names signal how these differ fromstruct_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()andcheck_types_on_init=Truenow recursively validate nestedStructinstances 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-bodystruct_configattribute. A plain Struct subclass with unknown class-statement kwargs now raisesTypeError(previously silently ignored).Custom metaclasses that subclass
StructMetacan still intercept and consume kwargs in their own__new__before callingsuper().__new__.
0.8.0 (2026-08-23)¶
Breaking: replace the
StructMetakeyword-argument config API with a pydantic-style class-bodystruct_configattribute.StructConfigis now aTypedDict(plain dict at runtime). The metaclass validates thatstruct_configis a dict, rejects unknown keys (catches typos), and requires strictboolvalues 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 subclassStructMetacan intercept and consume kwargs in their own__new__before callingsuper().__new__.Migration:
Old
New
class Point(Struct, frozen=True)class Point(Struct):struct_config = StructConfig(frozen=True)Point.__struct_config__.frozenPoint.__struct_config__["frozen"]New public API:
StructConfigis aTypedDict(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_configattribute on Struct types (resolved, fully-populated dict).kw_onlyandrenamenow 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:
StructConfigC object type (~500 lines of C deleted).StructConfig.replace()method (use dict merge{**cfg, ...}instead).UNSETsentinel 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_configsuffices.Note:
isinstance(x, StructConfig)raisesTypeErroron all Python versions (TypedDicts do not support instance checks). Useisinstance(x, dict)instead.Fix:
from __future__ import annotationsis now fully supported. Lazy string annotations are resolved at class creation, soSerializer(dump=...)codecs,Field(alias=...)metadata, andValidatorconstraints work exactly as with eager annotations. PreviouslySerializer(dump=...)codecs andField(alias=...)were silently dropped under lazy annotations.
0.7.0 (2026-08-22)¶
Breaking: Split
Fieldinto three focused annotation types:Field(identity + schema metadata),Serializer(value conversion), andValidator(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_lengthField(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, oneSerializer, and oneValidatorper annotation position; cross-kind combinations encouraged. Passing constraint params toFieldnow raisesTypeError.Breaking:
struct_validate_self()andvalidate_on_init=Trueare now pure type-checks. If a custom-typed field’s value is not already an instance of the declared type, aValidationErroris raised immediately —Serializer.loadand protocol fallbacks (struct_validate/model_validate) methods are not called. Any annotatedValidatoris still invoked on correctly-typed values. Previously,loadwas 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_nametoFieldInfo.alias.Breaking: remove
StrAdapter. For custom types constructed from a single string argument (IPv4Address,HttpUrl, …), useAnnotated[T, Field(dump=str, validate=T)]codecs instead.Breaking: remove the
gcStruct 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_hookkeyword arguments fromstruct_dump,struct_dump_json,struct_validate, andstruct_validate_jsonon bothStructandStructAdapter. Custom types now implement thestruct_dump/struct_validateprotocol methods (pydantic’smodel_dump/model_validateare also recognized), or use the new per-field codecs. Passing these arguments now raises aTypeError.Add
Field(dump=...)andField(validate=...)codecs, used asAnnotated[X, Field(dump=..., validate=...)]. Codecs may only be attached to custom types; attaching one to a natively supported type or to a union raises aTypeErrorat class creation time, as does attaching two conflictingdumpcodecs within a single field. Codecs are supported onStructfields only —StructAdapterrejects codec’d annotations (use astruct_dump/struct_validateprotocol method there).Remove
Field(default=...)andField(default_factory=...). Defaults are now always specified on the class body: a constant viax: int = 3, and a per-instance default by wrapping a callable in the newstructtype.Factory, e.g.x: list = Factory(list).Fieldis now a pure constraints/metadata object and may only be used insidetyping.Annotated.Add
structtype.Factory(func), a wrapper for per-instance default values.= Factory(list)is equivalent to the previousField(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__, useobject.__setattr__(self, ...)(requires Python 3.13+).Replace the encoder
order=argument withsort_keys: boolonstruct_dump,struct_dump_json.sort_keys=Truesorts dict keys and set elements for deterministic output; struct, dataclass, and object fields keep their declaration order. The previousorder='sorted'mode is removed.Add
tuplewith all builtin types usable instruct_dump.Improved validation error messages.
Fix possible memory leak.
Improve free threading support.
Add more safeguards to the C core.
Improve
FieldInforepr.
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.