> ## Documentation Index
> Fetch the complete documentation index at: https://nestcrate.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate request data with DTOs in nestrs

> Define validated request shapes with #[dto] and field attributes. Bind them to handlers via ValidatedBody, ValidatedPath, and ValidatedQuery extractors.

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:

```rust theme={null}
use nestrs::prelude::*;

#[dto]
pub struct CreateUserDto {
    #[IsEmail]
    pub email: String,

    #[Length(min = 1, max = 80)]
    pub name: String,

    #[Min(1)]
    pub age: i32,

    #[IsUrl]
    pub website: Option<String>,
}
```

`#[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.

<Note>
  `#[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`).
</Note>

## Available validation attributes

| Attribute | Equivalent validator rule | Notes |
| - | - | - |
| `#[IsEmail]` | `validate(email)` | |
| `#[IsUrl]` | `validate(url)` | |
| `#[IsUUID]` | `validate(custom(function = "nestrs::is_uuid"))` | Canonical `8-4-4-4-12` UUID check run at validation time on `String` fields; `uuid::Uuid` fields are serde-enforced and need no marker; any other field type is a compile error |
| `#[IsString]` | *(no-op)* | Readability marker; compile error on non-`String` fields |
| `#[IsBoolean]` | *(no-op)* | Readability marker; compile error on non-`bool` fields |
| `#[IsInt]` | *(no-op)* | Compile-checked: field must be an integer type (`i8`–`i128`, `u8`–`u128`, `isize`/`usize`) |
| `#[IsNumber]` | *(no-op)* | Compile-checked: field must be numeric (integers or `f32`/`f64`) |
| `#[IsNotEmpty]` | `validate(length(min = 1))` | |
| `#[IsPositive]` | `validate(range(min = 1))` | |
| `#[IsNegative]` | `validate(range(max = -1))` | |
| `#[Min(n)]` | `validate(range(min = n))` | |
| `#[Max(n)]` | `validate(range(max = n))` | |
| `#[MinLength(n)]` | `validate(length(min = n))` | |
| `#[MaxLength(n)]` | `validate(length(max = n))` | |
| `#[Length(min = m, max = n)]` | `validate(length(min = m, max = n))` | |
| `#[ValidateNested]` | `validate(nested)` | Recurse into a nested DTO field |
| `#[Matches(REGEX)]` | `validate(regex = REGEX)` | |
| `#[Contains(pat)]` | `validate(contains(pat))` | |
| `#[IsOptional]` | *(no-op — use `Option<T>`)* | Stripped at compile time |

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.

| NestJS | nestrs |
| - | - |
| `PartialType(CreateUserDto)` | `#[partial_type]` — wrap non-`Option` fields in `Option<T>` (PATCH body) |
| `OmitType(UserDto, ['password'])` | `#[omit_type(password)]` |
| `PickType(UserDto, ['email', 'name'])` | `#[pick_type(email, name)]` |
| `IntersectionType(IdentDto, ProfileDto)` | `#[intersection_type]` with parent fields |

`#[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()`.

```rust theme={null}
use nestrs::prelude::*;

#[dto(allow_unknown_fields)]
struct IdentDto {
    #[IsEmail]
    email: String,
}

#[dto(allow_unknown_fields)]
struct ProfileDto {
    #[Length(min = 2)]
    name: String,
}

#[nestrs::intersection_type]
struct CreateUserMergedDto {
    ident: IdentDto,
    profile: ProfileDto,
}
```

`CreateUserMergedDto` deserializes from `{"email": "ada@example.com", "name": "Ada"}`. Full reference: [DTO mapped types](/api/macros/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:

```rust theme={null}
use nestrs::prelude::*;
use std::sync::Arc;

#[dto]
pub struct CreateUserDto {
    #[IsEmail]
    pub email: String,
    #[Length(min = 1, max = 80)]
    pub name: String,
}

#[derive(serde::Serialize)]
pub struct UserResponse {
    pub email: String,
    pub name: String,
}

#[controller(prefix = "/users")]
pub struct UsersController;

#[routes(state = UsersService)]
impl UsersController {
    #[post("/")]
    pub async fn create(
        State(svc): State<Arc<UsersService>>,
        ValidatedBody(dto): ValidatedBody<CreateUserDto>,
    ) -> Json<UserResponse> {
        Json(svc.create(dto))
    }
}
```

## ValidatedPath and ValidatedQuery

The same pattern applies to path parameters and query strings:

<CodeGroup>
  ```rust validated path theme={null}
  #[dto]
  pub struct ItemParams {
      #[validate(range(min = 1))]
      pub id: i64,
  }

  #[routes(state = AppState)]
  impl ItemsController {
      #[get("/items/:id")]
      pub async fn get_item(
          ValidatedPath(params): ValidatedPath<ItemParams>,
      ) -> String {
          params.id.to_string()
      }
  }
  ```

  ```rust validated query theme={null}
  #[dto]
  pub struct SearchQuery {
      #[IsString]
      #[MinLength(3)]
      pub term: String,
  }

  #[routes(state = AppState)]
  impl SearchController {
      #[get("/search")]
      pub async fn search(
          ValidatedQuery(q): ValidatedQuery<SearchQuery>,
      ) -> String {
          q.term
      }
  }
  ```
</CodeGroup>

## Nested DTOs with ValidateNested

Use `#[ValidateNested]` on a field whose type is itself a `#[dto]` struct to trigger recursive validation:

```rust theme={null}
use nestrs::prelude::*;

#[dto]
pub struct AddressDto {
    #[IsString]
    #[IsNotEmpty]
    pub city: String,
}

#[dto]
pub struct RegisterDto {
    #[IsEmail]
    pub email: String,

    #[ValidateNested]
    pub address: AddressDto,
}
```

<Warning>
  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`.
</Warning>

## 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]`:

```rust theme={null}
#[routes(state = AppState)]
impl UsersController {
    #[post("/signup")]
    #[use_pipes(ValidationPipe)]
    pub async fn signup(#[param::body] dto: SignupDto) -> &'static str {
        "ok"
    }
}
```

Both styles produce the same `422` response shape when validation fails.

<Warning>
  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).
</Warning>

## 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 `HttpException`s are wrapped as `400 Bad Request`.

```rust theme={null}
use nestrs::prelude::*;

#[routes(state = AppState)]
impl UsersController {
    // Trim a raw JSON string body on the way in. `#[use_pipes]` records the
    // chain; the PipedBody1 extractor is what executes it.
    #[post("/names")]
    #[use_pipes(TrimPipe)]
    pub async fn names(raw: PipedBody1<String, TrimPipe>) -> String {
        format!("[{}]", raw.0)
    }

    // Coerce a path segment to i64 — "/users/abc" yields a 400 from ParseIntPipe.
    #[get("/users/:id")]
    pub async fn by_id(raw: PipedPath1<String, ParseIntPipe>) -> String {
        format!("id={}", raw.0)
    }
}
```

<Warning>
  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.
</Warning>

`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:

```rust theme={null}
#[dto(allow_unknown_fields)]
pub struct LooseDto {
    #[IsString]
    pub name: String,
}
```

<Tip>
  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.
</Tip>

## Error response shape

A failed validation returns `422 Unprocessable Entity` with a JSON body:

```json theme={null}
{
  "statusCode": 422,
  "message": "Validation failed",
  "error": "Unprocessable Entity",
  "errors": [
    {
      "field": "email",
      "constraints": {
        "email": "email"
      }
    }
  ]
}
```

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.

<AccordionGroup>
  <Accordion title="Full example: DTO + controller + module">
    ```rust theme={null}
    use nestrs::prelude::*;
    use std::sync::Arc;

    #[dto]
    pub struct CreateUserDto {
        #[IsEmail]
        pub email: String,
        #[Length(min = 1, max = 80)]
        pub name: String,
    }

    #[derive(serde::Serialize)]
    pub struct UserResponse {
        pub email: String,
        pub name: String,
    }

    #[derive(Default)]
    #[injectable]
    pub struct UsersService;

    impl UsersService {
        pub fn create(&self, dto: CreateUserDto) -> UserResponse {
            UserResponse {
                email: dto.email,
                name: dto.name,
            }
        }
    }

    #[controller(prefix = "/users")]
    pub struct UsersController;

    #[routes(state = UsersService)]
    impl UsersController {
        #[post("/")]
        pub async fn create(
            State(svc): State<Arc<UsersService>>,
            ValidatedBody(dto): ValidatedBody<CreateUserDto>,
        ) -> Result<Json<UserResponse>, HttpException> {
            Ok(Json(svc.create(dto)))
        }
    }

    #[module(
        controllers = [UsersController],
        providers = [UsersService],
    )]
    pub struct UsersModule;
    ```
  </Accordion>
</AccordionGroup>
