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

  1. Remove all {...} captures from the route so it is fully static, e.g. #[typed_path("/health")] struct HealthRoute;.
  2. 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).
  3. 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

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


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)