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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## [Unreleased]

### Added

- `jsonschema`: New `Validator::evaluate()` API exposes JSON Schema Output v1 (flag/list/hierarchical) reports along with iterator helpers for annotations and errors.
- `jsonschema-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).

### Removed

- `jsonschema`: The legacy `Validator::apply()`, `Output`, and `BasicOutput` types have been removed in favor of the richer `evaluate()` API.

## [0.35.0] - 2025-11-16

### Added
Expand Down
95 changes: 95 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,100 @@
# Migration Guide

## Upgrading from 0.35.x to 0.36.0

### Removal of `Validator::apply`, `Output`, and `BasicOutput`

The legacy `apply()` API and its `BasicOutput`/`OutputUnit` structures have been removed in favor of
the richer [`Validator::evaluate`](https://docs.rs/jsonschema/latest/jsonschema/struct.Validator.html#method.evaluate)
interface that exposes the JSON Schema Output v1 formats (flag/list/hierarchical) directly.

```rust
use serde_json::json;

// Old (0.35.x)
let output = validator.apply(&instance).basic();
match output {
BasicOutput::Valid(units) => println!("valid: {units:?}"),
BasicOutput::Invalid(errors) => println!("errors: {errors:?}"),
}

// New (0.36.0)
let evaluation = validator.evaluate(&instance);
if evaluation.flag().valid {
println!("valid");
}
let list = serde_json::to_value(evaluation.list())?;
let hierarchical = serde_json::to_value(evaluation.hierarchical())?;
```

Because `evaluate()` materializes every evaluation step so it can provide the structured outputs, it
always walks the full schema tree. If you only need a boolean result, continue to prefer
[`is_valid`](https://docs.rs/jsonschema/latest/jsonschema/fn.is_valid.html) or
[`validate`](https://docs.rs/jsonschema/latest/jsonschema/fn.validate.html).

The serialized JSON now matches the [JSON Schema Output v1 specification](https://github.com/json-schema-org/json-schema-spec/blob/main/specs/output/jsonschema-validation-output-machines.md)
and its companion [schema](https://github.com/json-schema-org/json-schema-spec/blob/main/specs/output/schema.json).
For example, evaluating an array against a schema with `prefixItems` and `items` produces list output like:

```json
{
"valid": false,
"details": [
{"valid": false, "evaluationPath": "", "schemaLocation": "", "instanceLocation": ""},
{
"valid": false,
"evaluationPath": "/items",
"instanceLocation": "",
"schemaLocation": "/items",
"droppedAnnotations": true
},
{
"valid": false,
"evaluationPath": "/items",
"instanceLocation": "/1",
"schemaLocation": "/items"
},
{
"valid": false,
"evaluationPath": "/items/type",
"instanceLocation": "/1",
"schemaLocation": "/items/type",
"errors": {"type": "\"oops\" is not of type \"integer\""}
},
{
"valid": true,
"evaluationPath": "/prefixItems",
"instanceLocation": "",
"schemaLocation": "/prefixItems",
"annotations": 0
},
{
"valid": true,
"evaluationPath": "/prefixItems/0",
"instanceLocation": "/0",
"schemaLocation": "/prefixItems/0"
},
{
"valid": true,
"evaluationPath": "/prefixItems/0/type",
"instanceLocation": "/0",
"schemaLocation": "/prefixItems/0/type"
},
{
"valid": true,
"evaluationPath": "/type",
"instanceLocation": "",
"schemaLocation": "/type"
}
]
}
```

If you need to inspect annotations or errors programmatically without serializing to JSON, use the
new [`evaluation.iter_annotations()`](https://docs.rs/jsonschema/latest/jsonschema/struct.Evaluation.html#method.iter_annotations)
and [`evaluation.iter_errors()`](https://docs.rs/jsonschema/latest/jsonschema/struct.Evaluation.html#method.iter_errors)
helpers.

## Upgrading from 0.34.x to 0.35.0

### Custom meta-schemas require explicit registration
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Boolean result
assert!(validator.is_valid(&instance));

// Structured output (JSON Schema Output v1)
let evaluation = validator.evaluate(&instance);
for annotation in evaluation.iter_annotations() {
eprintln!(
"Annotation at {}: {:?}",
annotation.schema_location,
annotation.annotations.value()
);
}

Ok(())
}
```
Expand All @@ -53,7 +63,7 @@ See more usage examples in the [documentation](https://docs.rs/jsonschema).
- 📚 Full support for popular JSON Schema drafts
- 🔧 Custom keywords and format validators
- 🌐 Blocking & non-blocking remote reference fetching (network/file)
- 🎨 `Basic` output style as per JSON Schema spec
- 🎨 Structured Output v1 reports (flag/list/hierarchical)
- ✨ Meta-schema validation for schema documents, including custom metaschemas
- 🔗 Bindings for [Python](https://github.com/Stranger6667/jsonschema/tree/master/crates/jsonschema-py)
- 🚀 WebAssembly support
Expand Down
7 changes: 6 additions & 1 deletion codecov.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ coverage:
project: off
patch: off

# Ignore test/benchmark infrastructure from coverage
# Ignore test/benchmark infrastructure & dev-only suite helpers from coverage
ignore:
- "crates/benchmark/"
- "crates/benchmark-suite/"
- "crates/jsonschema-testsuite/"
- "crates/jsonschema-testsuite-codegen/"
- "crates/jsonschema-testsuite-internal/"
- "crates/jsonschema-referencing-testsuite/"
- "crates/jsonschema-referencing-testsuite-codegen/"
- "crates/jsonschema-referencing-testsuite-internal/"
- "crates/testsuite-common/"
29 changes: 19 additions & 10 deletions crates/jsonschema-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ jsonschema [OPTIONS] <SCHEMA>

**NOTE**: It only supports valid JSON as input.

### Options:
### Options

- `-i, --instance <FILE>`: JSON instance(s) to validate (can be used multiple times)
- `--output <text|flag|list|hierarchical>`: Select output style (default: `text`). `text` prints the human-friendly summary, while the structured modes emit newline-delimited JSON (`ndjson`) records with `schema`, `instance`, and JSON Schema Output v1 payloads.
- `-v, --version`: Show version information
- `--help`: Display help information

Expand All @@ -37,6 +38,13 @@ Validate multiple instances:
jsonschema schema.json -i instance1.json -i instance2.json
```

Emit JSON Schema Output v1 (`list`) for multiple instances:
```
jsonschema schema.json -i instance1.json -i instance2.json --output list
{"output":"list","schema":"schema.json","instance":"instance1.json","payload":{"valid":true,...}}
{"output":"list","schema":"schema.json","instance":"instance2.json","payload":{"valid":false,...}}
```

## Features

- Validate one or more JSON instances against a single schema
Expand All @@ -45,17 +53,18 @@ jsonschema schema.json -i instance1.json -i instance2.json

## Output

For each instance, the tool will output:
For each instance:

- `<filename> - VALID` if the instance is valid
- `<filename> - INVALID` followed by a list of errors if invalid
- `text` (default): prints `<filename> - VALID` or `<filename> - INVALID. Errors:` followed by numbered error messages.
- `flag|list|hierarchical`: emit newline-delimited JSON objects shaped as:

Example output:
```
instance1.json - VALID
instance2.json - INVALID. Errors:
1. "name" is a required property
2. "age" must be a number
```json
{
"output": "list",
"schema": "schema.json",
"instance": "instance.json",
"payload": { "... JSON Schema Output v1 data ..." }
}
```

## Exit Codes
Expand Down
91 changes: 78 additions & 13 deletions crates/jsonschema-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{

use clap::{ArgAction, Parser, ValueEnum};
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
use serde_json::json;

#[derive(Parser)]
#[command(name = "jsonschema")]
Expand Down Expand Up @@ -47,11 +48,39 @@ struct Cli {
)]
no_assert_format: Option<bool>,

/// Select the output format (text, flag, list, hierarchical). All modes emit newline-delimited JSON records.
#[arg(
long = "output",
value_enum,
default_value_t = Output::Text,
help = "Select output style: text (default), flag, list, hierarchical"
)]
output: Output,

/// Show program's version number and exit.
#[arg(short = 'v', long = "version")]
version: bool,
}

#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum Output {
Text,
Flag,
List,
Hierarchical,
}

impl Output {
fn as_str(self) -> &'static str {
match self {
Output::Text => "text",
Output::Flag => "flag",
Output::List => "list",
Output::Hierarchical => "hierarchical",
}
}
}

#[derive(ValueEnum, Clone, Copy, Debug)]
enum Draft {
#[clap(name = "4")]
Expand Down Expand Up @@ -153,6 +182,7 @@ fn validate_instances(
schema_path: &Path,
draft: Option<Draft>,
assert_format: Option<bool>,
output: Output,
) -> Result<bool, Box<dyn std::error::Error>> {
let mut success = true;

Expand All @@ -168,19 +198,48 @@ fn validate_instances(
}
match options.build(&schema_json) {
Ok(validator) => {
for instance in instances {
let instance_json = read_json(instance)??;
let mut errors = validator.iter_errors(&instance_json);
let filename = instance.to_string_lossy();
if let Some(first) = errors.next() {
success = false;
println!("{filename} - INVALID. Errors:");
println!("1. {first}");
for (i, error) in errors.enumerate() {
println!("{}. {error}", i + 2);
if matches!(output, Output::Text) {
for instance in instances {
let instance_json = read_json(instance)??;
let mut errors = validator.iter_errors(&instance_json);
let filename = instance.to_string_lossy();
if let Some(first) = errors.next() {
success = false;
println!("{filename} - INVALID. Errors:");
println!("1. {first}");
for (i, error) in errors.enumerate() {
println!("{}. {error}", i + 2);
}
} else {
println!("{filename} - VALID");
}
}
} else {
let schema_display = schema_path.to_string_lossy().to_string();
let output_format = output.as_str();
for instance in instances {
let instance_json = read_json(instance)??;
let evaluation = validator.evaluate(&instance_json);
let flag_output = evaluation.flag();
let payload = match output {
Output::Text => unreachable!("handled above"),
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 instance_display = instance.to_string_lossy();
let record = json!({
"output": output_format,
"schema": &schema_display,
"instance": instance_display,
"payload": payload,
});
println!("{}", serde_json::to_string(&record)?);

if !flag_output.valid {
success = false;
}
} else {
println!("{filename} - VALID");
}
}
}
Expand All @@ -206,7 +265,13 @@ fn main() -> ExitCode {
// - Some(false) if --no-assert-format
// - None if neither (use builder’s default)
let assert_format = config.assert_format.or(config.no_assert_format);
return match validate_instances(&instances, &schema, config.draft, assert_format) {
return match validate_instances(
&instances,
&schema,
config.draft,
assert_format,
config.output,
) {
Ok(true) => ExitCode::SUCCESS,
Ok(false) => ExitCode::FAILURE,
Err(error) => {
Expand Down
Loading
Loading