Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

- **CLI**: Structured `--output flag|list|hierarchical` modes now stream newline-delimited JSON records with schema/instance metadata plus JSON Schema Output v1 payloads (default `text` output remains human-readable).
- **CLI**: `--errors-only` flag to suppress successful validation output and only show failures.
- **CLI**: When invoked with only a schema file (no instances), validates the schema against its meta-schema. [#804](https://github.com/Stranger6667/jsonschema/issues/804)
- New `Validator::evaluate()` API exposes JSON Schema Output v1 (flag/list/hierarchical) reports along with iterator helpers for annotations and errors.
- `meta::validator_for()` function to build validators for meta-schema validation with full `Validator` API access.
- `Validator` now implements `Clone`. [#809](https://github.com/Stranger6667/jsonschema/issues/809)

### Removed
Expand Down
77 changes: 75 additions & 2 deletions crates/jsonschema-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,65 @@ fn path_to_uri(path: &std::path::Path) -> String {
result
}

fn output_schema_validation(
schema_path: &Path,
schema_json: &serde_json::Value,
output: Output,
errors_only: bool,
) -> Result<bool, Box<dyn std::error::Error>> {
let meta_validator = jsonschema::meta::validator_for(schema_json)?;
let evaluation = meta_validator.evaluate(schema_json);
let flag_output = evaluation.flag();

// Skip valid schemas if errors_only is enabled
if !(errors_only && flag_output.valid) {
let schema_display = schema_path.to_string_lossy().to_string();
let output_format = output.as_str();
let payload = match output {
Output::Text => unreachable!("text mode should not call this function"),
Output::Flag => serde_json::to_value(flag_output)?,
Output::List => serde_json::to_value(evaluation.list())?,
Output::Hierarchical => serde_json::to_value(evaluation.hierarchical())?,
};

let record = json!({
"output": output_format,
"schema": &schema_display,
"payload": payload,
});
println!("{}", serde_json::to_string(&record)?);
}

Ok(flag_output.valid)
}

fn validate_schema_meta(
schema_path: &Path,
output: Output,
errors_only: bool,
) -> Result<bool, Box<dyn std::error::Error>> {
let schema_json = read_json(schema_path)??;

if matches!(output, Output::Text) {
// Text output mode
match jsonschema::meta::validate(&schema_json) {
Ok(()) => {
if !errors_only {
println!("Schema is valid");
}
Ok(true)
}
Err(error) => {
println!("Schema is invalid. Error: {error}");
Ok(false)
}
}
} else {
// Structured output modes using evaluate API
output_schema_validation(schema_path, &schema_json, output, errors_only)
}
}

fn validate_instances(
instances: &[PathBuf],
schema_path: &Path,
Expand Down Expand Up @@ -255,7 +314,12 @@ fn validate_instances(
}
}
Err(error) => {
println!("Schema is invalid. Error: {error}");
if matches!(output, Output::Text) {
println!("Schema is invalid. Error: {error}");
} else {
// Schema compilation failed - validate the schema itself to get structured output
output_schema_validation(schema_path, &schema_json, output, errors_only)?;
}
success = false;
}
}
Expand All @@ -274,7 +338,7 @@ fn main() -> ExitCode {
if let Some(instances) = config.instances {
// - Some(true) if --assert-format
// - Some(false) if --no-assert-format
// - None if neither (use builders default)
// - None if neither (use builder's default)
let assert_format = config.assert_format.or(config.no_assert_format);
return match validate_instances(
&instances,
Expand All @@ -292,6 +356,15 @@ fn main() -> ExitCode {
}
};
}
// No instances provided - validate the schema itself
return match validate_schema_meta(&schema, config.output, config.errors_only) {
Ok(true) => ExitCode::SUCCESS,
Ok(false) => ExitCode::FAILURE,
Err(error) => {
println!("Error: {error}");
ExitCode::FAILURE
}
};
}
ExitCode::SUCCESS
}
171 changes: 171 additions & 0 deletions crates/jsonschema-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,3 +713,174 @@ fn test_errors_only_structured_output() {
assert_eq!(records[0]["instance"], invalid);
assert_eq!(records[0]["payload"]["valid"], false);
}

#[test]
fn test_validate_valid_schema() {
let dir = tempdir().unwrap();
let schema = create_temp_file(&dir, "schema.json", r#"{"type": "string"}"#);

let mut cmd = cli();
cmd.arg(&schema);
let output = cmd.output().unwrap();
assert!(output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Schema is valid"));
}

#[test]
fn test_validate_invalid_schema() {
let dir = tempdir().unwrap();
let schema = create_temp_file(
&dir,
"schema.json",
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
);

let mut cmd = cli();
cmd.arg(&schema);
let output = cmd.output().unwrap();
assert!(!output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Schema is invalid"));
}

#[test]
fn test_instance_validation_with_invalid_schema_structured_output() {
let dir = tempdir().unwrap();
let schema = create_temp_file(
&dir,
"schema.json",
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
);
let instance = create_temp_file(&dir, "instance.json", "42");

let mut cmd = cli();
cmd.arg(&schema)
.arg("--instance")
.arg(&instance)
.arg("--output")
.arg("flag");
let output = cmd.output().unwrap();
assert!(!output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");

assert_eq!(json["output"], "flag");
assert_eq!(json["payload"]["valid"], false);
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
}

#[test]
fn test_instance_validation_with_invalid_schema_list_output() {
let dir = tempdir().unwrap();
let schema = create_temp_file(
&dir,
"schema.json",
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
);
let instance = create_temp_file(&dir, "instance.json", "42");

let mut cmd = cli();
cmd.arg(&schema)
.arg("--instance")
.arg(&instance)
.arg("--output")
.arg("list");
let output = cmd.output().unwrap();
assert!(!output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");

assert_eq!(json["output"], "list");
assert_eq!(json["payload"]["valid"], false);
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
}

#[test]
fn test_instance_validation_with_invalid_schema_hierarchical_output() {
let dir = tempdir().unwrap();
let schema = create_temp_file(
&dir,
"schema.json",
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
);
let instance = create_temp_file(&dir, "instance.json", "42");

let mut cmd = cli();
cmd.arg(&schema)
.arg("--instance")
.arg(&instance)
.arg("--output")
.arg("hierarchical");
let output = cmd.output().unwrap();
assert!(!output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");

assert_eq!(json["output"], "hierarchical");
assert_eq!(json["payload"]["valid"], false);
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
}

#[test]
fn test_validate_invalid_schema_list_output() {
let dir = tempdir().unwrap();
let schema = create_temp_file(
&dir,
"schema.json",
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
);

let mut cmd = cli();
cmd.arg(&schema).arg("--output").arg("list");
let output = cmd.output().unwrap();
assert!(!output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");

assert_eq!(json["output"], "list");
assert_eq!(json["payload"]["valid"], false);
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
}

#[test]
fn test_validate_invalid_schema_hierarchical_output() {
let dir = tempdir().unwrap();
let schema = create_temp_file(
&dir,
"schema.json",
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
);

let mut cmd = cli();
cmd.arg(&schema).arg("--output").arg("hierarchical");
let output = cmd.output().unwrap();
assert!(!output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");

assert_eq!(json["output"], "hierarchical");
assert_eq!(json["payload"]["valid"], false);
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
}

#[test]
fn test_validate_schema_with_json_parse_error() {
let dir = tempdir().unwrap();
let schema = create_temp_file(&dir, "schema.json", r#"{"type": "string"#);

let mut cmd = cli();
cmd.arg(&schema).arg("--output").arg("flag");
let output = cmd.output().unwrap();
assert!(!output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Error:"));
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
source: crates/jsonschema-cli/tests/cli.rs
expression: "String::from_utf8_lossy(&output.stdout)"
---

Schema is valid
Loading
Loading