Skip to content

Add serde support for Command and builder types #6299

@macisamuele

Description

@macisamuele

Please complete the following tasks

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_serde appears 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-enhancementCategory: Raise on the bar on expectationsS-triageStatus: New; needs maintainer attention.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions