Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add JSON Schema support #1287

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

yasamoka
Copy link

@yasamoka yasamoka commented Mar 8, 2025

First of all, thank you for your wonderful work on secrecy!

This PR adds JSON Schema support to SecretString and SecretBox<T> using schemars.

My use case for JSON Schema is validation of JSON configuration files used by a web server.

Here is a simplified example of how this works.

We start with a Config struct that uses both SecretString and SecretBox:

#[derive(Debug, Deserialize, JsonSchema)]
pub struct Config {
    pub username: SecretString,
    pub password: SecretString,
    pub number: SecretBox<u64>,
}

We derive JsonSchema in order to add JSON Schema support.

Then, we export a schema:

use std::{fs::write, path::Path};

use schemars::schema_for;
use serde_json::to_string_pretty;

use example::Config;

fn main() -> anyhow::Result<()> {
    let path = Path::new("config.schema.json");
    let schema = schema_for!(Config);
    let schema = to_string_pretty(&schema)?;
    write(path, schema)?;
    Ok(())
}

The schema produced looks like this:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Config",
  "type": "object",
  "required": [
    "number",
    "password",
    "username"
  ],
  "properties": {
    "number": {
      "$ref": "#/definitions/SecretBox_for_uint64"
    },
    "password": {
      "$ref": "#/definitions/SecretString"
    },
    "username": {
      "$ref": "#/definitions/SecretString"
    }
  },
  "definitions": {
    "SecretBox_for_uint64": {
      "type": "integer",
      "format": "uint64",
      "minimum": 0.0
    },
    "SecretString": {
      "type": "string"
    }
  }
}

We then add a .vscode/settings.json file registering the schema and applying it against config.json:

{
    "json.schemas": [
        {
            "fileMatch": [
                "config.json",
            ],
            "url": "./config.schema.json",
        },
    ]
}

Now, the contents of config.json are validated against the schema:

Missing property Incorrect type

Once the validation rules are satisfied, we end up with this configuration:

{
  "username": "foo",
  "password": "bar",
  "number": 123
}

Finally, we can deserialize the configuration using serde and use the credentials provided, as normal:

use std::{fs::read_to_string, path::Path};

use serde_json::from_str;

use example::Config;

fn main() -> anyhow::Result<()> {
    let config = read_to_string(Path::new("config.json"))?;
    let config = from_str::<Config>(config.as_str())?;
    println!("{config:?}",);
    Ok(())
}

I hope this is a suitable contribution to the project and would appreciate feedback if there are any improvements or fixes needed.

@yasamoka
Copy link
Author

yasamoka commented Mar 8, 2025

It seems that the MSRV has to be at least 1.61.0 due to jsonschema v0.8.22 -> serde_json 1.0.217 -> memchr v2.7.4.

However, other packages seem to also require a MSRV higher than 1.60.0, since I get the following error whenever I try to run cargo test --release --all-features on the main branch:

error: package rustix v0.38.44 cannot be built because it requires rustc 1.63 or newer, while the currently active rustc version is 1.60.0

@tony-iqlusion
Copy link
Member

It won't scale for secrecy to support arbitrary 3rd party crates like this. We added optional serde because it's so extremely pervasive in the Rust ecosystem, but this is a much more niche application.

You can get around the problem by copying and pasting the relevant types into your code and adding the relevant trait impls there.

@yasamoka
Copy link
Author

yasamoka commented Mar 11, 2025

It won't scale for secrecy to support arbitrary 3rd party crates like this. We added optional serde because it's so extremely pervasive in the Rust ecosystem, but this is a much more niche application.

You can get around the problem by copying and pasting the relevant types into your code and adding the relevant trait impls there.

This isn't an arbitrary 3rd-party crate. This covers a very common use case of secrecy, which is carrying secrets from the starting point of a configuration file onwards, and is within the realm of improved developer experience that the Rust ecosystem provides all over. This isn't some slippery slope of introducing 3rd-party crate support all over the place. Anyway, thank you for your review.

@tony-iqlusion
Copy link
Member

The other way to do it would be to open a PR to schemars

@yasamoka
Copy link
Author

yasamoka commented Mar 11, 2025

The other way to do it would be to open a PR to schemars

I think it would be out of scope to open a PR there, since schemars, much like serde, is simply for serialization (JSON Schema) / deserialization (JSON) and it's up to whoever wants to support these to add support in their own crates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants