Skip to content

Commit 368ce7c

Browse files
committed
feat: Evaluate API
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
1 parent 83000f3 commit 368ce7c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+5692
-2157
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- `jsonschema`: New `Validator::evaluate()` API exposes JSON Schema Output v1 (flag/list/hierarchical) reports along with iterator helpers for annotations and errors.
8+
- `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).
9+
10+
### Removed
11+
12+
- `jsonschema`: The legacy `Validator::apply()`, `Output`, and `BasicOutput` types have been removed in favor of the richer `evaluate()` API.
13+
514
## [0.35.0] - 2025-11-16
615

716
### Added

MIGRATION.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,100 @@
11
# Migration Guide
22

3+
## Upgrading from 0.35.x to 0.36.0
4+
5+
### Removal of `Validator::apply`, `Output`, and `BasicOutput`
6+
7+
The legacy `apply()` API and its `BasicOutput`/`OutputUnit` structures have been removed in favor of
8+
the richer [`Validator::evaluate`](https://docs.rs/jsonschema/latest/jsonschema/struct.Validator.html#method.evaluate)
9+
interface that exposes the JSON Schema Output v1 formats (flag/list/hierarchical) directly.
10+
11+
```rust
12+
use serde_json::json;
13+
14+
// Old (0.35.x)
15+
let output = validator.apply(&instance).basic();
16+
match output {
17+
BasicOutput::Valid(units) => println!("valid: {units:?}"),
18+
BasicOutput::Invalid(errors) => println!("errors: {errors:?}"),
19+
}
20+
21+
// New (0.36.0)
22+
let evaluation = validator.evaluate(&instance);
23+
if evaluation.flag().valid {
24+
println!("valid");
25+
}
26+
let list = serde_json::to_value(evaluation.list())?;
27+
let hierarchical = serde_json::to_value(evaluation.hierarchical())?;
28+
```
29+
30+
Because `evaluate()` materializes every evaluation step so it can provide the structured outputs, it
31+
always walks the full schema tree. If you only need a boolean result, continue to prefer
32+
[`is_valid`](https://docs.rs/jsonschema/latest/jsonschema/fn.is_valid.html) or
33+
[`validate`](https://docs.rs/jsonschema/latest/jsonschema/fn.validate.html).
34+
35+
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)
36+
and its companion [schema](https://github.com/json-schema-org/json-schema-spec/blob/main/specs/output/schema.json).
37+
For example, evaluating an array against a schema with `prefixItems` and `items` produces list output like:
38+
39+
```json
40+
{
41+
"valid": false,
42+
"details": [
43+
{"valid": false, "evaluationPath": "", "schemaLocation": "", "instanceLocation": ""},
44+
{
45+
"valid": false,
46+
"evaluationPath": "/items",
47+
"instanceLocation": "",
48+
"schemaLocation": "/items",
49+
"droppedAnnotations": true
50+
},
51+
{
52+
"valid": false,
53+
"evaluationPath": "/items",
54+
"instanceLocation": "/1",
55+
"schemaLocation": "/items"
56+
},
57+
{
58+
"valid": false,
59+
"evaluationPath": "/items/type",
60+
"instanceLocation": "/1",
61+
"schemaLocation": "/items/type",
62+
"errors": {"type": "\"oops\" is not of type \"integer\""}
63+
},
64+
{
65+
"valid": true,
66+
"evaluationPath": "/prefixItems",
67+
"instanceLocation": "",
68+
"schemaLocation": "/prefixItems",
69+
"annotations": 0
70+
},
71+
{
72+
"valid": true,
73+
"evaluationPath": "/prefixItems/0",
74+
"instanceLocation": "/0",
75+
"schemaLocation": "/prefixItems/0"
76+
},
77+
{
78+
"valid": true,
79+
"evaluationPath": "/prefixItems/0/type",
80+
"instanceLocation": "/0",
81+
"schemaLocation": "/prefixItems/0/type"
82+
},
83+
{
84+
"valid": true,
85+
"evaluationPath": "/type",
86+
"instanceLocation": "",
87+
"schemaLocation": "/type"
88+
}
89+
]
90+
}
91+
```
92+
93+
If you need to inspect annotations or errors programmatically without serializing to JSON, use the
94+
new [`evaluation.iter_annotations()`](https://docs.rs/jsonschema/latest/jsonschema/struct.Evaluation.html#method.iter_annotations)
95+
and [`evaluation.iter_errors()`](https://docs.rs/jsonschema/latest/jsonschema/struct.Evaluation.html#method.iter_errors)
96+
helpers.
97+
398
## Upgrading from 0.34.x to 0.35.0
499

5100
### Custom meta-schemas require explicit registration

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3434
// Boolean result
3535
assert!(validator.is_valid(&instance));
3636

37+
// Structured output (JSON Schema Output v1)
38+
let evaluation = validator.evaluate(&instance);
39+
for annotation in evaluation.iter_annotations() {
40+
eprintln!(
41+
"Annotation at {}: {:?}",
42+
annotation.schema_location,
43+
annotation.annotations.value()
44+
);
45+
}
46+
3747
Ok(())
3848
}
3949
```
@@ -53,7 +63,7 @@ See more usage examples in the [documentation](https://docs.rs/jsonschema).
5363
- 📚 Full support for popular JSON Schema drafts
5464
- 🔧 Custom keywords and format validators
5565
- 🌐 Blocking & non-blocking remote reference fetching (network/file)
56-
- 🎨 `Basic` output style as per JSON Schema spec
66+
- 🎨 Structured Output v1 reports (flag/list/hierarchical)
5767
- ✨ Meta-schema validation for schema documents, including custom metaschemas
5868
- 🔗 Bindings for [Python](https://github.com/Stranger6667/jsonschema/tree/master/crates/jsonschema-py)
5969
- 🚀 WebAssembly support

codecov.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ coverage:
66
project: off
77
patch: off
88

9-
# Ignore test/benchmark infrastructure from coverage
9+
# Ignore test/benchmark infrastructure & dev-only suite helpers from coverage
1010
ignore:
1111
- "crates/benchmark/"
1212
- "crates/benchmark-suite/"
1313
- "crates/jsonschema-testsuite/"
14+
- "crates/jsonschema-testsuite-codegen/"
15+
- "crates/jsonschema-testsuite-internal/"
1416
- "crates/jsonschema-referencing-testsuite/"
17+
- "crates/jsonschema-referencing-testsuite-codegen/"
18+
- "crates/jsonschema-referencing-testsuite-internal/"
19+
- "crates/testsuite-common/"

crates/jsonschema-cli/README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ jsonschema [OPTIONS] <SCHEMA>
1919

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

22-
### Options:
22+
### Options
2323

2424
- `-i, --instance <FILE>`: JSON instance(s) to validate (can be used multiple times)
25+
- `--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.
2526
- `-v, --version`: Show version information
2627
- `--help`: Display help information
2728

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

41+
Emit JSON Schema Output v1 (`list`) for multiple instances:
42+
```
43+
jsonschema schema.json -i instance1.json -i instance2.json --output list
44+
{"output":"list","schema":"schema.json","instance":"instance1.json","payload":{"valid":true,...}}
45+
{"output":"list","schema":"schema.json","instance":"instance2.json","payload":{"valid":false,...}}
46+
```
47+
4048
## Features
4149

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

4654
## Output
4755

48-
For each instance, the tool will output:
56+
For each instance:
4957

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

53-
Example output:
54-
```
55-
instance1.json - VALID
56-
instance2.json - INVALID. Errors:
57-
1. "name" is a required property
58-
2. "age" must be a number
61+
```json
62+
{
63+
"output": "list",
64+
"schema": "schema.json",
65+
"instance": "instance.json",
66+
"payload": { "... JSON Schema Output v1 data ..." }
67+
}
5968
```
6069

6170
## Exit Codes

crates/jsonschema-cli/src/main.rs

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{
88

99
use clap::{ArgAction, Parser, ValueEnum};
1010
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
11+
use serde_json::json;
1112

1213
#[derive(Parser)]
1314
#[command(name = "jsonschema")]
@@ -47,11 +48,39 @@ struct Cli {
4748
)]
4849
no_assert_format: Option<bool>,
4950

51+
/// Select the output format (text, flag, list, hierarchical). All modes emit newline-delimited JSON records.
52+
#[arg(
53+
long = "output",
54+
value_enum,
55+
default_value_t = Output::Text,
56+
help = "Select output style: text (default), flag, list, hierarchical"
57+
)]
58+
output: Output,
59+
5060
/// Show program's version number and exit.
5161
#[arg(short = 'v', long = "version")]
5262
version: bool,
5363
}
5464

65+
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
66+
enum Output {
67+
Text,
68+
Flag,
69+
List,
70+
Hierarchical,
71+
}
72+
73+
impl Output {
74+
fn as_str(self) -> &'static str {
75+
match self {
76+
Output::Text => "text",
77+
Output::Flag => "flag",
78+
Output::List => "list",
79+
Output::Hierarchical => "hierarchical",
80+
}
81+
}
82+
}
83+
5584
#[derive(ValueEnum, Clone, Copy, Debug)]
5685
enum Draft {
5786
#[clap(name = "4")]
@@ -153,6 +182,7 @@ fn validate_instances(
153182
schema_path: &Path,
154183
draft: Option<Draft>,
155184
assert_format: Option<bool>,
185+
output: Output,
156186
) -> Result<bool, Box<dyn std::error::Error>> {
157187
let mut success = true;
158188

@@ -168,19 +198,48 @@ fn validate_instances(
168198
}
169199
match options.build(&schema_json) {
170200
Ok(validator) => {
171-
for instance in instances {
172-
let instance_json = read_json(instance)??;
173-
let mut errors = validator.iter_errors(&instance_json);
174-
let filename = instance.to_string_lossy();
175-
if let Some(first) = errors.next() {
176-
success = false;
177-
println!("{filename} - INVALID. Errors:");
178-
println!("1. {first}");
179-
for (i, error) in errors.enumerate() {
180-
println!("{}. {error}", i + 2);
201+
if matches!(output, Output::Text) {
202+
for instance in instances {
203+
let instance_json = read_json(instance)??;
204+
let mut errors = validator.iter_errors(&instance_json);
205+
let filename = instance.to_string_lossy();
206+
if let Some(first) = errors.next() {
207+
success = false;
208+
println!("{filename} - INVALID. Errors:");
209+
println!("1. {first}");
210+
for (i, error) in errors.enumerate() {
211+
println!("{}. {error}", i + 2);
212+
}
213+
} else {
214+
println!("{filename} - VALID");
215+
}
216+
}
217+
} else {
218+
let schema_display = schema_path.to_string_lossy().to_string();
219+
let output_format = output.as_str();
220+
for instance in instances {
221+
let instance_json = read_json(instance)??;
222+
let evaluation = validator.evaluate(&instance_json);
223+
let flag_output = evaluation.flag();
224+
let payload = match output {
225+
Output::Text => unreachable!("handled above"),
226+
Output::Flag => serde_json::to_value(flag_output)?,
227+
Output::List => serde_json::to_value(evaluation.list())?,
228+
Output::Hierarchical => serde_json::to_value(evaluation.hierarchical())?,
229+
};
230+
231+
let instance_display = instance.to_string_lossy();
232+
let record = json!({
233+
"output": output_format,
234+
"schema": &schema_display,
235+
"instance": instance_display,
236+
"payload": payload,
237+
});
238+
println!("{}", serde_json::to_string(&record)?);
239+
240+
if !flag_output.valid {
241+
success = false;
181242
}
182-
} else {
183-
println!("{filename} - VALID");
184243
}
185244
}
186245
}
@@ -206,7 +265,13 @@ fn main() -> ExitCode {
206265
// - Some(false) if --no-assert-format
207266
// - None if neither (use builder’s default)
208267
let assert_format = config.assert_format.or(config.no_assert_format);
209-
return match validate_instances(&instances, &schema, config.draft, assert_format) {
268+
return match validate_instances(
269+
&instances,
270+
&schema,
271+
config.draft,
272+
assert_format,
273+
config.output,
274+
) {
210275
Ok(true) => ExitCode::SUCCESS,
211276
Ok(false) => ExitCode::FAILURE,
212277
Err(error) => {

0 commit comments

Comments
 (0)