-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add serde support for Command and builder types #6299
Description
Please complete the following tasks
- I have searched the discussions
- I have searched the open and rejected issues
Clap Version
4.5.60
Describe your use case
A concrete use case is building a proxy binary that fronts multiple specialized binaries behind a single CLI entry point, where each specific binary becomes a subcommand of the proxy.
The proxy needs to provide shell completions, help text, and argument validation (acceptable if not complete) without shelling out to the underlying binaries on every invocation. However, directly linking the specific binaries (or even just their clap derives) into the proxy is not viable because:
- Binary size: some binaries pull in heavy dependencies (e.g. C++ bindings) that would bloat the proxy far beyond what a thin dispatcher needs
- Traceability: the actual work must still be performed by the specific binary for auditing and process-tracking purposes
- Scalability: as the number of proxied binaries grows, linking them all becomes impractical
With serde support, each binary can export its Command definition to a file (JSON, TOML, etc.) at build time. The proxy binary then deserializes these specs, mounts them as subcommands, and gets full CLI integration (help, completions, validation) for free — without any source-level dependency on the specific binaries.
Related
- Dynamic subcommands #5109 — "Dynamic subcommands": requested loading command definitions from files at runtime, noted
clap_serdeappears deprecated
Describe the solution you'd like
Add an optional serde cargo feature that conditionally derives serde::Serialize and serde::Deserialize on Command and all related subtypes (Arg, ArgGroup, ArgAction, ArgPredicate, ValueRange, ValueHint, PossibleValue, StyledStr, Str, OsStr, Id, etc.).
Fields that are inherently non-serializable (trait objects like ValueParser, type-erased Extensions, function pointers) would be skipped with #[serde(skip)].
Example usage
// In each specific binary: export the CLI spec
let cmd = Command::new("my-tool")
.version("1.0")
.arg(Arg::new("input").long("input").action(ArgAction::Set));
let json = serde_json::to_string_pretty(&cmd)?;
std::fs::write("my-tool.cli.json", &json)?;
// In the proxy binary: reconstruct and mount as subcommands
let spec = std::fs::read_to_string("my-tool.cli.json")?;
let subcmd: Command = serde_json::from_str(&spec)?;
let proxy = Command::new("proxy")
.subcommand(subcmd)
.get_matches();Alternatives, if applicable
No response
Additional Context
There is currently no built-in way to serialize/deserialize Command, Arg, ArgGroup, and related builder types.
This makes it difficult to build tooling that needs to inspect, transform, or reconstruct CLI definitions outside of the original binary.
Third-party crates like clap-serde and clap-serde-derive attempt to fill this gap but appear unmaintained and don't provide comprehensive coverage of clap's builder types.