tokio-rs/axum · error · compile_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 (no fields) but the `#[typed_path("...")]` template contains one or more `{capture}` segments. A unit struct cannot carry captured values (there is nowhere to store them), so the `expand_unit_fields` branch (typed_path.rs:282) iterates `parse_path` and, on the first `Segment::Capture`, returns this error at line 290. The same iteration is what powers the unit-struct code path that otherwise emits a constant path with no formatting.
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 c9a911b799)
Solutions
- Remove the `{...}` captures from the path string so it is fully static, e.g. `#[typed_path("/users")]`.
- If you need the capture, give the struct fields to hold it: switch to a named-field struct (`struct MyPath { id: u32 }`) or a tuple struct (`struct MyPath(u32);`) matching the capture count.
- Verify the segment syntax — `*{name}` wildcards and `{name}` captures both count as captures and both trigger this error on unit structs.
- Re-read the path string for stray `{`/`}` (including doubled `{{` used to escape, which are intentionally not treated as captures).
Example fix
// before
#[derive(TypedPath)]
#[typed_path("/users/{id}")]
struct MyPath;
// after (option A: static route)
#[derive(TypedPath)]
#[typed_path("/users")]
struct MyPath;
// after (option B: give it a field)
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/{id}")]
struct MyPath { id: u32 } Defensive patterns
Strategy: validation
Validate before calling
// Rule: unit structs may only use capture-less paths. Enforce with a helper macro
// that refuses `{` in the path when no fields are declared:
macro_rules! static_typed_path {
($name:ident, $path:literal) => {
const _: () = { assert!(!$path.contains('{'), "unit typed path must be static"); };
#[derive(::axum_macros::TypedPath)]
#[typed_path($path)]
struct $name;
};
}
// Usage: static_typed_path!(Health, "/health");
// static_typed_path!(Bad, "/users/{id}"); // compile-time panic inside macro Type guard
// Compile-time assertion tying the path string to the absence of captures for unit structs.
// Place next to the derive; if someone later adds a {capture}, this stops compiling.
const _: () = {
const PATH: &str = "/users";
const HAS_CAPTURE: bool = PATH.contains('{');
const _: () = assert!(!HAS_CAPTURE, "unit TypedPath must not contain captures");
}; Prevention
- Reserve unit-struct `TypedPath`s for static routes only (e.g. `/health`, `/users`); any `{...}` means you need a field-carrying struct.
- When converting a parameterized route into a typed path, choose a named-field or tuple struct whose field count matches the capture count.
- Add a `const _: () = assert!(!PATH.contains('{'));` guard next to every unit typed path to catch regressions at compile time.
- Remember `*{name}` wildcards are also captures and will trip the same error on unit structs.
When it happens
Trigger: Annotated a unit struct (e.g. `struct MyPath;`) with `#[derive(TypedPath)]` and a path containing a capture such as `#[typed_path("/users/{id}")]`. The `Fields::Unit` arm at typed_path.rs:48 routes into `expand_unit_fields`, which calls `parse_path` (line 287) and on the first `Segment::Capture(_, span)` returns the error at line 290. Reproducible with `tests/typed_path/fail/unit_with_capture.rs`.
Common situations: Starting from a parameterized route (`/users/{id}`) and slimming the struct down to a unit marker without also dropping the capture from the path. Copy-pasting a `#[typed_path(...)]` line from a named/tuple struct onto a unit struct. Misunderstanding that `TypedPath` on a unit struct is meant only for static, capture-less routes (e.g. `/health`, `/users`).
Related errors
- Missing path: `#[typed_path("/foo/bar")]`
- can't infer state type, please add set it explicitly, as in
- can't infer state type, please add `#[{attr_name}(state = My
- missing `#[from_request(via(...))]`
AI-assisted analysis of tokio-rs/axum@c9a911b799 (2026-08-06).
Data as JSON: /data/errors/fe0a890768da6e03.json.
Report an issue: GitHub.