Validation and custom extractors
Separate parsing errors from business rules and return safe HTTP errors.
Typed extraction converts data to a Rust type. Business rules, such as a field being nonempty, a user existing, or two fields being consistent, are validated separately.
Validate the DTO's shape#
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct CreateUser {
name: String,
}deny_unknown_fields is an explicit application choice. Ordinary Json<T> does not automatically enable this rule for every DTO. Content-type mismatches, malformed JSON, and typed conversion errors are handled by the extractor.
Validate business rules#
The action in the controller example checks that the name is not empty:
if input.name.trim().is_empty() {
return Err(HttpApiError::ValidationError("name is required".into()));
}ValidationError produces HTTP 422. Do not assume its payload string is returned to the client verbatim; built-in errors use a safe public-message contract. If you need a public field-level error schema, define a custom error converter.
Reusable extractors#
| Need | Trait |
|---|---|
| Validate a request part without consuming the body | FromRequestParts |
| Treat the absence of that part as optional | OptionalFromRequestParts |
| Produce a new data type by consuming the body | FromRequest |
A custom extractor's Rejection type must implement Into<HttpApiError>. A custom body extractor must be the last typed parameter. Do not move request/body references outside the operation.
Choose the right error type#
| Situation | Typical HTTP result |
|---|---|
| Malformed data or typed conversion | 400 |
| Unsupported content type | 415 |
| Body limit exceeded | 413 |
| Business validation in this example | 422 |
Lily's extension points are extractors, guards, middleware, and response converters. Although the explanation order may feel familiar from NestJS, ValidationPipe is not part of Lily's API.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.