Built-in Extractors
Type-safe request data extraction — path params, query strings, JSON body, raw bytes, and validated inputs.
Extractors pull typed data out of the HTTP request. They implement the FromRequest trait, are parsed once per request, and produce compile-time type errors if you misuse them.
Path — route parameters
Extracts typed values from URL path segments:
use toni::extractors::Path;
// Single param
#[get("/:id")]
fn find_one(&self, Path(id): Path<u32>) -> HttpResponse {
// id is parsed to u32 from the URL segment
}
// Multiple params via a struct
#[derive(serde::Deserialize)]
struct PostParams {
user_id: u32,
post_id: u32,
}
#[get("/:user_id/posts/:post_id")]
fn find_post(&self, Path(params): Path<PostParams>) -> HttpResponse {
let user_id = params.user_id;
let post_id = params.post_id;
// ...
}Query — query string parameters
Deserializes the query string into a typed struct:
use toni::extractors::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct SearchQuery {
q: String,
page: Option<u32>,
limit: Option<u32>,
}
#[get("/search")]
fn search(&self, Query(params): Query<SearchQuery>) -> HttpResponse {
let q = params.q;
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(20);
// ...
}Json — strict JSON body
Parses the request body as JSON, requiring Content-Type: application/json:
use toni::extractors::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUserDto {
name: String,
email: String,
}
#[post("")]
fn create(&self, Json(dto): Json<CreateUserDto>) -> HttpResponse {
// dto is fully typed; if Content-Type is wrong or JSON is malformed, a 400 is returned
}Body — auto-detecting body
Accepts both JSON and application/x-www-form-urlencoded — auto-detects from the Content-Type header:
use toni::extractors::Body;
#[post("")]
fn create(&self, Body(dto): Body<CreateUserDto>) -> HttpResponse {
// works for both JSON and form-encoded bodies
}Use Json when you only want to accept JSON. Use Body when your endpoint needs to support form submissions as well.
Bytes — raw binary data
Returns the raw request body as a Vec<u8>:
use toni::extractors::Bytes;
#[post("/upload")]
fn upload(&self, Bytes(data): Bytes) -> HttpResponse {
let size = data.len();
// process raw bytes — useful for file uploads, webhooks, etc.
HttpResponse::ok()
.json(serde_json::json!({ "received_bytes": size }))
.build()
}Validated — extraction + validation
Wraps any extractor and runs validator crate validation before passing the data to the handler. If validation fails, a 422 response is returned automatically.
use toni::extractors::{Validated, Json};
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)]
struct CreateUserDto {
#[validate(length(min = 2, max = 100))]
name: String,
#[validate(email)]
email: String,
#[validate(range(min = 13, max = 120))]
age: u32,
}
#[post("")]
fn create(&self, Validated(Json(dto)): Validated<Json<CreateUserDto>>) -> HttpResponse {
// If any validation rule fails, handler is never called.
// dto.name is guaranteed to be 2-100 chars, dto.email is a valid email, etc.
HttpResponse::created()
.json(serde_json::json!({ "name": dto.name }))
.build()
}Validated can wrap any extractor that implements ValidatableExtractor:
Validated<Json<Dto>> // validate JSON body
Validated<Query<Dto>> // validate query params
Validated<Body<Dto>> // validate auto-detected bodyMultiple extractors
A handler can use multiple extractors simultaneously:
#[derive(Deserialize)]
struct PaginationQuery {
page: Option<u32>,
per_page: Option<u32>,
}
#[put("/:id")]
fn update(
&self,
Path(id): Path<u32>,
Query(pagination): Query<PaginationQuery>,
Validated(Json(dto)): Validated<Json<UpdateItemDto>>,
) -> HttpResponse {
// All three are parsed and available
}Custom extractors
Implement FromRequest to create your own extractor for any request data:
use toni::extractors::FromRequest;
use toni::HttpRequest;
pub struct CurrentUser {
pub id: u32,
pub email: String,
}
impl FromRequest for CurrentUser {
type Error = String;
fn from_request(req: &HttpRequest) -> Result<Self, Self::Error> {
let user_id: u32 = req.headers
.get("x-user-id")
.ok_or("Missing x-user-id header")?
.parse()
.map_err(|_| "Invalid user id")?;
Ok(CurrentUser {
id: user_id,
email: req.headers
.get("x-user-email")
.cloned()
.unwrap_or_default(),
})
}
}
// Use it exactly like any built-in extractor
#[get("/me")]
fn profile(&self, user: CurrentUser) -> HttpResponse {
HttpResponse::ok()
.json(serde_json::json!({ "id": user.id, "email": user.email }))
.build()
}See Custom Extractors for more patterns.