tokio-rs/axum · error · syn::Error
Typed paths for unit structs cannot contain captures
Error message
Typed paths for unit structs cannot contain captures
What it means
Thrown by #[derive(TypedPath)] when the struct is a unit struct (struct Foo;) and the route contains a capture (typed_path.rs:290). expand dispatches on Fields::Unit to expand_unit_fields, which iterates the parsed path segments; any Segment::Capture (a {...} or {*...} parameter) is rejected because a unit struct has no fields to store the captured value. Only fully static routes are valid for unit structs.
Solutions
- Remove all {...} captures from the route so it is fully static, e.g. #[typed_path("/health")] struct HealthRoute;.
- If you need the captured value, change the unit struct to a named struct with a field per capture, or a tuple struct with one field per capture (counts must match).
- For a catch-all/wildcard route, use a named/tuple struct with the matching field rather than a unit struct.
Example fix
// before
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/{id}")]
struct MyPath;
// after (option A: drop the capture, keep unit struct)
#[derive(TypedPath)]
#[typed_path("/users")]
struct UsersIndex;
// after (option B: keep the capture, add a field)
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/{id}")]
struct MyPath {
id: u32,
} Defensive patterns
Strategy: validation
Validate before calling
// Safe invocation pattern: unit structs (struct X;) need a fully static route — no captures.
#[derive(TypedPath)]
#[typed_path("/health")] // no {param}
struct Health;
// If the route has captures, do NOT use a unit struct; match each capture with a field:
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/{id}")]
struct UserShow {
id: u32,
}
// Tuple-struct variant (one field per capture, in order):
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/{id}")]
struct UserShow(u32); Prevention
- For unit struct typed paths, keep the route fully static: no {param}, no {*wildcard}.
- When a route gains a capture, change the struct from unit to named/tuple with one field per capture (counts must match).
- Review routes during refactors: turning a parameterized struct into a unit struct silently breaks if the route still has captures.
- Treat unit typed paths as 'match-only' routes (e.g. fixed endpoints like /health, /version).
When it happens
Trigger: #[typed_path("/users/{id}")] struct MyPath; — a unit struct whose route has one or more {param} or {*param} captures. Each capture segment hits the Segment::Capture(_, span) arm and returns the error.
Common situations: Declaring a typed path for a fixed route but leaving a stray {param} in it. Converting a field-bearing TypedPath struct to a unit struct without simplifying the route. Confusing a unit typed path (route match only) with a parameterized one.
Related errors
- Missing path: #[typed_path("/foo/bar")]
- can't infer state type, please add #
- can't infer state type, please add set it explicitly, as in…
- missing #[from_request(via(...))]
AI-assisted analysis of tokio-rs/axum@151cd5c123 (2026-08-11).
Data as JSON: /api/errors/fe0a890768da6e03.
Report an issue: GitHub.
Appendix: source
Thrown at axum-macros/src/typed_path.rs:290
}
fn simple_pluralize(count: usize, word: &str) -> String {
if count == 1 {
format!("{count} {word}")
} else {
format!("{count} {word}s")
}
}
fn expand_unit_fields(
ident: &syn::Ident,
path: &LitStr,
rejection: Option<&syn::Path>,
) -> syn::Result<TokenStream> {
for segment in parse_path(path)? {
match segment {
Segment::Capture(_, span) => {
return Err(syn::Error::new(
span,
"Typed paths for unit structs cannot contain captures",
));
}
Segment::Static(_) => {}
}
}
let typed_path_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
}
};
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {View on GitHub (pinned to 151cd5c123)