{"id":"fe0a890768da6e03","repo":"tokio-rs/axum","slug":"typed-paths-for-unit-structs-cannot-contain-captur","errorCode":null,"errorMessage":"Typed paths for unit structs cannot contain captures","messagePattern":"Typed paths for unit structs cannot contain captures","errorType":"validation","errorClass":"compile_error","httpStatus":null,"severity":"error","filePath":"axum-macros/src/typed_path.rs","lineNumber":290,"sourceCode":"}\n\nfn simple_pluralize(count: usize, word: &str) -> String {\n    if count == 1 {\n        format!(\"{count} {word}\")\n    } else {\n        format!(\"{count} {word}s\")\n    }\n}\n\nfn expand_unit_fields(\n    ident: &syn::Ident,\n    path: &LitStr,\n    rejection: Option<&syn::Path>,\n) -> syn::Result<TokenStream> {\n    for segment in parse_path(path)? {\n        match segment {\n            Segment::Capture(_, span) => {\n                return Err(syn::Error::new(\n                    span,\n                    \"Typed paths for unit structs cannot contain captures\",\n                ));\n            }\n            Segment::Static(_) => {}\n        }\n    }\n\n    let typed_path_impl = quote_spanned! {path.span()=>\n        #[automatically_derived]\n        impl ::axum_extra::routing::TypedPath for #ident {\n            const PATH: &'static str = #path;\n        }\n    };\n\n    let display_impl = quote_spanned! {path.span()=>\n        #[automatically_derived]\n        impl ::std::fmt::Display for #ident {","sourceCodeStart":272,"sourceCodeEnd":308,"githubUrl":"https://github.com/tokio-rs/axum/blob/c9a911b7999de50e9e5023942ca072e9725ae943/axum-macros/src/typed_path.rs#L272-L308","documentation":"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.","triggerScenarios":"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`.","commonSituations":"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`).","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)."],"exampleFix":"// before\n#[derive(TypedPath)]\n#[typed_path(\"/users/{id}\")]\nstruct MyPath;\n\n// after (option A: static route)\n#[derive(TypedPath)]\n#[typed_path(\"/users\")]\nstruct MyPath;\n\n// after (option B: give it a field)\n#[derive(TypedPath, Deserialize)]\n#[typed_path(\"/users/{id}\")]\nstruct MyPath { id: u32 }","handlingStrategy":"validation","validationCode":"// Rule: unit structs may only use capture-less paths. Enforce with a helper macro\n// that refuses `{` in the path when no fields are declared:\nmacro_rules! static_typed_path {\n    ($name:ident, $path:literal) => {\n        const _: () = { assert!(!$path.contains('{'), \"unit typed path must be static\"); };\n        #[derive(::axum_macros::TypedPath)]\n        #[typed_path($path)]\n        struct $name;\n    };\n}\n// Usage: static_typed_path!(Health, \"/health\");\n// static_typed_path!(Bad, \"/users/{id}\"); // compile-time panic inside macro","typeGuard":"// Compile-time assertion tying the path string to the absence of captures for unit structs.\n// Place next to the derive; if someone later adds a {capture}, this stops compiling.\nconst _: () = {\n    const PATH: &str = \"/users\";\n    const HAS_CAPTURE: bool = PATH.contains('{');\n    const _: () = assert!(!HAS_CAPTURE, \"unit TypedPath must not contain captures\");\n};","tryCatchPattern":null,"preventionTips":["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."],"tags":["rust","axum","proc-macro","typed-path","routing","unit-struct","compile-time"],"analyzedSha":"c9a911b7999de50e9e5023942ca072e9725ae943","analyzedAt":"2026-08-06T01:08:56.656Z","schemaVersion":2}