Skip to content

feat (flag:schemars): adding feature gated support for json schema generation #151

@aRustyDev

Description

@aRustyDev

PR Response: Addressing Concerns about Schema Generation

@epage Thank you for reviewing my PR, and apologies for not following the contrib guidelines, I got to the fix first and wanted to share it back.

I'd like to clarify the motivation and address your concerns about the schemars integration.

The Core Problem: Private Fields Block Downstream Solutions

You mentioned it would be "trivial to handle that on the caller side," but this isn't actually possible due to the private fields in Verbosity. When attempting remote derivation (the standard way to handle external types):

// 1. Create 'duplicate' of the target struct, w/ different name
// 2.  Tie it to the "remote" 
// Result: This approach fails because Verbosity's fields are private
// Note: this also breaks "continuous" documentation ability, since the user now needs to track+react to any upstream changes
#[derive(schemars::JsonSchema)]
#[schemars(remote = "clap_verbosity_flag::Verbosity")]
struct VerbositySchemaDef {
    quiet: Option<bool>,  // Error: field `quiet` is private
    verbose: Option<u8>,  // Error: field `verbose` is private
}

The only alternatives are:

  1. Newtype wrapper - loses all schema information:
#[derive(schemars::JsonSchema)]
struct MyVerbosity(#[schemars(skip)] clap_verbosity_flag::Verbosity);
// Schema: {} - completely opaque, defeats the purpose
  1. Manual implementation - brittle and breaks with upstream changes:
impl schemars::JsonSchema for MyVerbosity {
    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
        // Manually guessing internal structure - will break
    }
}

Why JSON Schemas Matter for CLI Tools

Modern CLI applications commonly support both command-line arguments AND configuration files. Consider tools like:

  • Docker Compose (docker-compose.yml mirrors CLI flags)
  • GitHub Actions (workflow files with verbosity settings)
  • Cargo itself (Cargo.toml with verbose output options)

When a CLI tool accepts configuration through files, having a schema enables:

  • IDE autocomplete and validation
  • CI/CD pipeline validation
  • Documentation generation
  • LLM-assisted configuration (increasingly important as AI tools help users configure complex systems)

Admittedly, this being a "verbosity" flag crate, does kind of stand apart from these values mentioned. BUT the big problem is that due to the way schemars works, not supporting its option would mean one of 3 things

  1. the user doesn't use this crate, b/c it will break schemars functionality
  2. the user forks this crate and uses the fork
  3. the user doesn't use schemars at all

How Schemars Handles This Correctly

Per the [schemars documentation](https://graham.cool/schemars/):

"One of the main aims of this library is compatibility with Serde. Any generated schema should match how serde_json would serialize/deserialize to/from JSON."

The schema generated with this PR accurately represents how the Verbosity struct serializes:

// With this PR, users can do:
#[derive(serde::Deserialize, schemars::JsonSchema)]
struct AppConfig {
    database_url: String,
    #[serde(flatten)]
    verbosity: clap_verbosity_flag::Verbosity,
    // ... other config
}

// Generates schema showing:
{
  "properties": {
    "database_url": { "type": "string" },
    "quiet": { "type": "boolean", "nullable": true },
    "verbose": { "type": "integer", "nullable": true }
  }
}

This schema correctly represents that in a config file, users would specify:

database_url: "postgres://..."
verbose: 2  # Equivalent to -vv on CLI

Addressing "CLIs are not well handled by schemars"

I understand the concern about semantic differences between CLI args and JSON schemas. However, schemars isn't trying to represent CLI argument semantics - it's representing the data structure that results from parsing those arguments.

The schema accurately describes the Verbosity struct's shape after clap has parsed the CLI args, which is exactly what's needed when that same struct is used in configuration files.

Zero Cost for Non-Users

This change is behind a feature flag:

[dependencies]
clap-verbosity-flag = "2.0"  # No extra dependencies

# Only users who need it pay the cost:
clap-verbosity-flag = { version = "2.0", features = ["schemars"] }

Summary

This PR enables a common use case (CLI tools with config files) that is currently impossible to implement downstream due to private fields. The implementation:

  • ✅ Is semantically correct (matches serde serialization)
  • ✅ Has zero cost unless explicitly opted into
  • ✅ Follows established patterns used by other clap-related crates
  • ✅ Enables modern tooling integration (IDEs, validation, LLMs)

Without this change, users must either fork the crate or vendor it locally (which is what I had to do to make my application work). The alternative of making fields public would be a breaking change, while this PR solves the problem without breaking anyone.

Would you consider this given that it's blocking legitimate use cases and has no impact on users who don't need it?

Also, I have made some additional changes to remove the serde:Serialization, serde:Deserialization from the derive statements.
I had moved those over as a misunderstanding during my search for the bug fix that led me here, but on reading your response and testing further I determined it wasn't necessary, so I yanked it.

Related to #150

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions