Skip to main content
nestrs DTO validation combines serde for deserialization with the validator crate for constraint checking. The #[dto] macro derives both automatically; ValidationPipe or ValidatedBody<T> runs the validation before your handler receives the data. Invalid payloads return 422 Unprocessable Entity with a structured error body.

#[dto]

Derives serde::Deserialize, validator::Validate, and NestDto on a struct. By default it also emits #[serde(deny_unknown_fields)] so any JSON key not in the struct definition causes a 422 error.
use nestrs::prelude::*;

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

    #[Length(min = 3, max = 32)]
    username: String,

    #[IsOptional]
    #[IsInt]
    age: Option<i64>,
}

#[dto(allow_unknown_fields)]

Opts out of deny_unknown_fields. Use this when you intentionally accept JSON payloads from forward-compatible clients that may include extra fields.
#[dto(allow_unknown_fields)]
struct PartialUpdateDto {
    #[IsString]
    name: Option<String>,
}
Do not manually add #[serde(deny_unknown_fields)] to a struct that already uses #[dto]—the macro applies it by default and duplicating the attribute causes a compile error.

NestDto trait

NestDto is a marker trait generated by #[dto]. It is used by ValidationPipe and the ValidatedBody<T>, ValidatedQuery<T>, and ValidatedPath<T> extractors to enforce that only DTOs marked with #[dto] are passed through the validation pipeline.
pub trait NestDto {}
You do not implement NestDto manually; #[dto] handles it.

Using DTOs in handlers

use nestrs::prelude::*;

#[dto]
struct SignupDto {
    #[IsEmail]
    email: String,
}

#[routes(state = AppState)]
impl AuthController {
    #[post("/signup")]
    async fn signup(
        ValidatedBody(dto): ValidatedBody<SignupDto>,
    ) -> &'static str {
        let _ = dto;
        "created"
    }
}

Field validation attributes

All field attributes are applied inside a #[dto] struct. They expand to validator crate constraint annotations.

String constraints

#[IsString]
attribute
Validates that the field is a non-empty string. Useful as a presence check when used with String.
#[IsEmail]
attribute
Validates that the field is a well-formed email address.
#[IsUrl]
attribute
Validates that the field is a well-formed URL.
#[Length(min = N, max = N)]
attribute
Validates string length. Both min and max are optional.
#[dto]
struct ProfileDto {
    #[IsEmail]
    email: String,

    #[IsUrl]
    website: String,

    #[Length(min = 1, max = 255)]
    bio: String,
}

Numeric constraints

#[IsInt]
attribute
Validates that the field is an integer type (i8i128, u8u128, isize, usize).
#[IsNumber]
attribute
Validates that the field is a numeric type (also accepts f32, f64).
#[Min(N)]
attribute
Validates that the numeric field is greater than or equal to N.
#[Max(N)]
attribute
Validates that the numeric field is less than or equal to N.
#[dto]
struct PriceDto {
    #[IsNumber]
    #[Min(0)]
    amount: f64,

    #[IsInt]
    #[Min(1)]
    #[Max(1000)]
    quantity: i64,
}

Boolean and optional

#[IsBoolean]
attribute
Validates that the field is a bool.
#[IsOptional]
attribute
Marks the field as optional in the validation pipeline. Wrap the field type in Option<T> and add this attribute so that a missing JSON key is accepted without triggering other validators on the field.
#[dto]
struct UpdateDto {
    #[IsOptional]
    #[IsString]
    name: Option<String>,

    #[IsOptional]
    #[IsBoolean]
    active: Option<bool>,
}

Nested DTOs

#[ValidateNested]
attribute
Runs validation recursively on a nested struct field. The nested type must also derive Validate (which #[dto] provides).
#[dto]
struct AddressDto {
    #[IsString]
    street: String,

    #[IsString]
    city: String,
}

#[dto]
struct OrderDto {
    #[IsString]
    product_id: String,

    #[ValidateNested]
    shipping_address: AddressDto,
}

Comprehensive example

The following DTO covers string, numeric, boolean, optional, and nested validation in a single type:
use nestrs::prelude::*;

#[dto]
struct ContactInfoDto {
    #[IsString]
    first_name: String,

    #[IsString]
    last_name: String,

    #[IsEmail]
    email: String,

    #[IsOptional]
    #[IsUrl]
    website: Option<String>,

    #[IsInt]
    #[Min(18)]
    #[Max(120)]
    age: i64,

    #[IsOptional]
    #[IsBoolean]
    newsletter: Option<bool>,
}

#[dto]
struct CreateContactDto {
    #[ValidateNested]
    contact: ContactInfoDto,

    #[IsOptional]
    #[Length(max = 500)]
    notes: Option<String>,
}

#[controller(prefix = "/contacts")]
struct ContactsController;

#[routes(state = AppState)]
impl ContactsController {
    #[post("/")]
    #[http_code(201)]
    async fn create(
        ValidatedBody(dto): ValidatedBody<CreateContactDto>,
    ) -> axum::Json<serde_json::Value> {
        axum::Json(serde_json::json!({ "created": true }))
    }
}

deny_unknown_fields default behavior

#[dto] applies #[serde(deny_unknown_fields)] by default. This means any JSON key in the request body that is not a field on the struct causes deserialization to fail with a 422 response before validation even runs.
// Request body — fails with 422 if `#[dto]` is used (no allow_unknown_fields)
{
  "email": "user@example.com",
  "unexpected_field": "value"
}
This is intentional and matches NestJS’s ValidationPipe behavior with whitelist: true. Use #[dto(allow_unknown_fields)] when you need to accept extra fields from clients on a different schema version.