Skip to content

Feature/attribute passthrough - #248

Merged
greyblake merged 2 commits into
masterfrom
feature/attribute-passthrough
Jun 13, 2026
Merged

Feature/attribute passthrough#248
greyblake merged 2 commits into
masterfrom
feature/attribute-passthrough

Conversation

@greyblake

@greyblake greyblake commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Attribute passthrough and serde customization

Attributes written on the struct and on the inner field are now forwarded verbatim onto the generated type, so the compiler and third-party derives (via derive_unchecked) can read them.
Previously struct attributes were rejected and field attributes were silently dropped.
This makes nutype compose with crates like sqlx, garde and zeroize without nutype having to know about them. (In the longer run the plan is to support such popular crates natively, with invariant-aware derives that do not require derive_unchecked; passthrough is the general escape hatch that works
today.)

#[nutype(
    sanitize(trim),
    derive(Debug, Clone),
    derive_unchecked(garde::Validate),
)]
pub struct UserId(#[garde(length(min = 1))] String);

Serde attributes are the exception: nutype hand-writes its Serialize/Deserialize impls, so instead of forwarding, it now understands field-level #[serde(with = "...")], serialize_with, deserialize_with and type-level #[serde(transparent)] natively. Sanitization and validation always still run on deserialization, even with a custom deserialize_with function, so the core guarantee is preserved:

#[nutype(
    validate(predicate = |v| !v.is_empty()),
    derive(Debug, Serialize, Deserialize),
)]
pub struct InvitationToken(#[serde(with = "base64_codec")] Vec<u8>);

Closes #229, partially address #228, closes #201. Also unblocks the sqlx(transparent) and zeroize asks in #191 and makes #214 (borsh) easier. Covered by a new functional suite (including guarantee tests proving validation gates custom deserialization), UI fixtures for every targeted error, and third-party derive proofs in examples/derive_unchecked_example. Targeted at 0.8.0, since previously dropped field attributes becoming live is an observable behavior change. Also fixes a latent bug where field attributes on custom ("any") inner types leaked into type positions of the generated code.

Implements attribute passthrough (#229):

* Struct-level attributes (e.g. #[repr(transparent)], #[sqlx(transparent)])
  and field-level attributes (e.g. #[garde(length(min = 1))]) are forwarded
  verbatim onto the generated type, so third-party derives pulled in via
  derive_unchecked can read them. Previously struct attributes were rejected
  and field attributes were silently dropped. Resolves #228 and the
  attribute half of #191.
  Forwarded attributes are emitted after the generated #[derive(...)],
  because derive-helper attributes are only legal after the derive that
  introduces them (legacy_derive_helpers).
* Serde customization (#201): nutype's generated Serialize/Deserialize
  impls honor field-level #[serde(with = "...")], serialize_with,
  deserialize_with and struct-level #[serde(transparent)], using serde's own
  syntax. The serialize_with path mirrors serde's derive (a wrapper struct
  inside newtype framing); transparent drops the framing entirely.
  Sanitization and validation ALWAYS still run on deserialization: a custom
  deserialize_with function only produces the raw inner value, which is then
  routed through try_new/new.
* Other serde keys, #[schemars(...)], #[cfg] on the inner field and native
  #[derive(...)] get targeted errors. Item-level #[cfg] needs no handling:
  rustc strips it before the macro expands (pinned by a functional test).
* Fix: field attributes on "any" inner types no longer leak into type
  positions of the generated code (AnyInnerType carried the whole syn::Field
  including attributes, which broke e.g. fn try_new(raw_value: #[attr] T)).

Tests: functional suite in test_suite/tests/attr_passthrough.rs (forwarding,
serde matrix incl. RON framing, guarantee tests proving validation gates
custom deserialization), UI fixtures for every targeted error plus an
observable forwarding proof via deny(deprecated), and third-party derive
proofs (arbitrary field attr, derive_more::Display type attr) in
examples/derive_unchecked_example. The test_suite deliberately does not gain
a derive_unchecked feature: several UI fixtures pin the feature-off error
messages and would flip under --all-features.
README and macro docs: new "Attribute passthrough" section with sqlx/garde
recipes, the serde customization subsection, and the boundary note (nutype
forwards attributes verbatim and cannot verify what they do; an attribute
macro that rewrites the type can break the guarantees, exactly like
derive_unchecked).
@greyblake
greyblake merged commit 1a7edfb into master Jun 13, 2026
6 checks passed
@TheBestTvarynka

Copy link
Copy Markdown

@greyblake, thank you!

@greyblake

greyblake commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Available now in 0.8.0-beta.1.
I'd be happy if someone could it a shot and test it :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bypass macro attributes Support serde attributes

2 participants