#[dto] proc-macro that turns a plain Rust struct into a fully-typed, validated Data Transfer Object. It derives Deserialize, Serialize, Validate (from the validator crate), and NestDto — and it enables a set of NestJS-style field attributes (#[IsEmail], #[Length], etc.) so your validation intent is visible at the declaration site rather than buried in implementation code.
Defining a DTO
Annotate a struct with#[dto] and add validation attributes to each field:
#[dto] expands to a struct with #[serde(deny_unknown_fields)] by default. Any JSON body that contains a key not declared on the struct returns a 400 Bad Request before your handler runs.
#[IsString] is a readability marker only — Rust’s type system already enforces that a String field is a string. On a String (or Option<String>) field it compiles to no validation code; on any other field type the #[dto] macro rejects the useless marker at compile time. The same compile-time guard applies to #[IsBoolean] (non-bool fields), #[IsInt] / #[IsNumber] (non-numeric fields), and #[IsUUID] (fields that are neither String nor uuid::Uuid).Available validation attributes
You can also use raw
validator attributes directly: #[validate(range(min = 0))] works on any #[dto] field.
Mapped types (PartialType / OmitType / PickType / IntersectionType)
NestJS @nestjs/mapped-types maps to four macros. Partial / Omit / Pick re-list the source fields on a new struct. Intersection does not — parent DTOs are flattened into one JSON object.
#[intersection_type] emits #[serde(flatten)] + #[validate(nested)] on each parent field. Parent DTOs must use #[dto(allow_unknown_fields)] or flattened siblings fail each other’s deny_unknown_fields. Nested validator errors live under ValidationErrors::errors(), not field_errors().
CreateUserMergedDto deserializes from {"email": "ada@example.com", "name": "Ada"}. Full reference: DTO mapped types.
Using ValidatedBody in a controller
PairValidatedBody<T> with your DTO type as an Axum extractor. nestrs validates the deserialized value before your handler body runs and returns 422 Unprocessable Entity on failure:
ValidatedPath and ValidatedQuery
The same pattern applies to path parameters and query strings:Nested DTOs with ValidateNested
Use#[ValidateNested] on a field whose type is itself a #[dto] struct to trigger recursive validation:
Using ValidationPipe explicitly
ValidatedBody, ValidatedPath, and ValidatedQuery run validation inline as part of extraction. If you prefer the NestJS #[use_pipes(ValidationPipe)] style you can use it with #[param::body] / #[param::query] / #[param::param]:
422 response shape when validation fails.
Other pipes: TrimPipe, ParseIntPipe, and explicit chains
#[use_pipes(...)] auto-rewrites #[param::body] / #[param::query] / #[param::param] parameters only for the ValidationPipe fast path above. For any other chain, decorated parameters keep their raw Json / Query / Path extractors — pipes named in the attribute do not run on them. To run a real pipe chain at extraction time, type the parameter as one of the Piped* extractors directly: PipedBody1..PipedBody4, PipedQuery1..PipedQuery4, and PipedPath1..PipedPath4 accept a chain of one to four pipes, run them in declaration order (each pipe transforms the previous pipe’s output), and short-circuit on the first failure with that pipe’s own status code — a 400 from ParseIntPipe, a 422 from ValidationPipe. Pipe errors that are not HttpExceptions are wrapped as 400 Bad Request.
TrimPipe (strips leading/trailing whitespace from a String) ships with the framework alongside ParseIntPipe and ValidationPipe, all importable from nestrs::prelude. Pipes implement PipeTransform<Input> from nestrs::core; the HTTP extractors additionally require the marker trait HttpPipeTransform<Input>, which adds the Default bound the extractors use to instantiate each pipe without going through DI. (WS and microservice transports have their own WsPipeTransform / MicroPipeTransform sub-traits.) Custom pipes implement both traits for each input type they accept, with an Error type that implements std::error::Error.
Allowing unknown fields
By default#[dto] adds #[serde(deny_unknown_fields)] so clients cannot send undocumented keys. To opt out:
Error response shape
A failed validation returns422 Unprocessable Entity with a JSON body:
errors entry pairs the failing field with a constraints map of rule code → message. The validator crate does not attach a default message to its built-in rules, so the message falls back to the rule code ("email", "length", …) unless the rule sets one explicitly, e.g. #[validate(email(message = "must be a valid email address"))] or #[Length(min = 1, message = "...")] via #[dto] passthrough.
Full example: DTO + controller + module
Full example: DTO + controller + module