Skip to main content
nestrs ships a #[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

Pair ValidatedBody<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:
Recursive validation only fires if the nested struct also derives Validate. #[dto] handles this automatically — but if you hand-write a nested struct, make sure it derives validator::Validate.

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]:
Both styles produce the same 422 response shape when validation fails.
The fast path triggers whenever ValidationPipe appears anywhere in the chain: #[use_pipes(TrimPipe, ValidationPipe)] on a #[param::body] parameter rewrites it to ValidatedBody exactly like the single-pipe form, and TrimPipe does not run on that parameter. Put non-ValidationPipe pipes on an explicit Piped* extractor instead (see below).

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.
Do not combine #[param::body] / #[param::query] / #[param::param] with a Piped* type. #[routes] would wrap the parameter in a raw axum::Json<T> / Query<T> / Path<T> extractor that must deserialize the Piped* type itself — which it cannot. The Piped* extractor is the Axum extractor: declare it bare, as above.
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:
Keep deny_unknown_fields enabled (the default) for public-facing APIs to prevent clients from smuggling extra data and to catch typos in field names early.

Error response shape

A failed validation returns 422 Unprocessable Entity with a JSON body:
Each 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.