#[partial_type], #[omit_type(...)], #[pick_type(...)], and #[intersection_type] mirror NestJS’s @nestjs/mapped-types (PartialType, OmitType, PickType, IntersectionType). Partial/Omit/Pick emit the same derive set as #[dto] (Debug, Clone, Serialize, Deserialize, NestDto, JsonSchema, Validate) plus the default #[serde(deny_unknown_fields)]. Field-level validator markers (#[IsEmail], #[Length(...)], #[Min(...)], etc.) are preserved on every surviving field. #[intersection_type] omits deny_unknown_fields because serde forbids it on structs that contain #[serde(flatten)].
#[partial_type]
Wraps every non-optional field in Option<T>. Fields that are already Option<T> are left as-is — Option<Option<T>> would be a bug, and we avoid it. The result is the standard “PATCH body” shape: every field is independent, omitting a field skips its inner validator, and supplying null for a non-optional field is a deserialization error.
UpdateUserDto deserializes from any subset of the four fields. A request body with {"email": "ada@example.com"} succeeds; one with {"email": null} fails with 400 Bad Request because email is Option<String>, not Option<Option<String>>. An empty body {} succeeds because every field is optional.
Validation behavior on Option fields
When a field is Some(_), its inner validator runs. When it is None, the validator is skipped — the entire point of a partial type. This matches NestJS’s PartialType semantics and validator 0.21’s behavior on Option<T> fields.
#[omit_type(field_a, field_b, ...)]
Builds a new DTO by removing the named fields from the source struct. Surviving fields keep their type and their validators. This is the typed equivalent of writing a duplicate DTO with one fewer field — same derive set, same deny_unknown_fields behavior, but the field list lives in the macro arguments.
CreateUserNoNicknameDto has exactly email, name, and age. Listing a field name that does not exist on the source struct is a compile error — the macro matches against the field list of the struct it is applied to.
Common use
The most common case is a “registration without optional fields” DTO: a single source DTO defines all fields, and an omit variant is used at a stricter endpoint (e.g., admin onboarding) where the optional fields must be set later.#[pick_type(field_a, field_b, ...)]
The inverse of #[omit_type]. Keeps only the named fields; everything else is dropped. Same derive set, same validator preservation.
UserIdentityDto has exactly email and name. Pick is useful for “view model” DTOs that project a subset of a wider input — for example, the public-safe fields of an internal entity.
Common use
#[intersection_type]
NestJS IntersectionType(A, B) analogue. Unlike Partial/Omit/Pick, this does not re-list every leaf field. Each named field is a parent DTO that is #[serde(flatten)]’d into one JSON object, so { ...A, ...B } deserializes as a single body. Nested Validate runs via #[validate(nested)]. Requires at least two named fields; takes no arguments.
Parent DTOs must be declared with #[dto(allow_unknown_fields)]. Flattened siblings would otherwise fail each other’s deny_unknown_fields (each parent would treat the other’s keys as unknown).
CreateUserMergedDto deserializes from {"email": "ada@example.com", "name": "Ada"}. Constructing it in Rust still uses the nested fields (ident / profile); only the JSON shape is flat. Nested validator failures live under ValidationErrors::errors() (struct-level), not field_errors().
How the macros relate to #[dto]
Partial/Omit/Pick call the same internal code path as #[dto]:
- The struct is rewritten:
PartialTypewraps non-Option fields inOption<T>;OmitTypefilters the field list by name;PickTypefilters the field list by name in the opposite direction. - The validator attributes (
#[IsEmail],#[Length(...)],#[Min(...)], etc.) on every surviving field are translated tovalidator0.21#[validate(...)]attributes, exactly as#[dto]does. - The macro emits
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, nestrs::NestDto, nestrs::schemars::JsonSchema, validator::Validate)]plus#[serde(deny_unknown_fields)].
#[intersection_type] emits the same derive set without deny_unknown_fields, and injects #[serde(flatten)] + #[validate(nested)] on each parent field instead of rewriting leaf types.
This means ValidatedBody<T> / ValidatedQuery<T> / ValidationPipe accept mapped-type DTOs without any extra configuration, and the resulting types flow into nestrs-openapi components.schemas automatically through the JsonSchema derive.
Unlike NestJS, where
@nestjs/mapped-types re-types the source class by reference, nestrs Partial/Omit/Pick take the source shape inline. You re-list the fields on the new struct. This is the standalone macro pattern, mirroring ts-rs and specta in the Rust ecosystem — it avoids cross-crate path resolution at proc-macro time and keeps the field-level validator markers editable per output struct. #[intersection_type] is the exception: parent DTOs are referenced by field type, not re-listed leaf by leaf.