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

  1. Remove the `{...}` captures from the path string so it is fully static, e.g. `#[typed_path("/users")]`.
  2. 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.
  3. Verify the segment syntax — `*{name}` wildcards and `{name}` captures both count as captures and both trigger this error on unit structs.
  4. 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

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


AI-assisted analysis of tokio-rs/axum@c9a911b799 (2026-08-06). Data as JSON: /data/errors/fe0a890768da6e03.json. Report an issue: GitHub.