Skip to content

Commit de9cd5e

Browse files
committed
fix(validate): address review feedback for --path option
Signed-off-by: Vaibhav mittal <vaibhavmittal929@gmail.com>
1 parent caaa025 commit de9cd5e

File tree

10 files changed

+186
-26
lines changed

10 files changed

+186
-26
lines changed

docs/validate.markdown

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ jsonschema validate <schema.json|.yaml> <instance.json|.jsonl|.yaml|directory...
1111
[--benchmark/-b] [--loop <iterations>] [--extension/-e <extension>]
1212
[--ignore/-i <schemas-or-directories>] [--trace/-t] [--fast/-f]
1313
[--template/-m <template.json>] [--json/-j] [--entrypoint/-p <pointer|uri>]
14+
[--path/-P <pointer>]
1415
```
1516

1617
The most popular use case of JSON Schema is to validate JSON documents. The
@@ -190,3 +191,24 @@ jsonschema validate path/to/my/schema.json path/to/instances/ \
190191
jsonschema validate path/to/my/schema.json path/to/my/instance.json \
191192
--entrypoint '/$defs/MyType'
192193
```
194+
195+
### Extract and validate against a sub-schema using a JSON Pointer
196+
197+
The `--path`/`-P` option extracts a sub-schema from the input document using a
198+
[JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901) before validation. This
199+
is useful for validating instances against schemas embedded in larger documents,
200+
such as OpenAPI specifications.
201+
202+
```sh
203+
jsonschema validate path/to/openapi.json path/to/instance.json \
204+
--path '/components/schemas/User'
205+
```
206+
207+
The JSON Pointer must resolve to a value in the document that is a valid JSON
208+
Schema. If the pointer does not resolve, the CLI will report an error with the
209+
attempted pointer path.
210+
211+
> [!WARNING]
212+
> Extracting a sub-schema with `--path` may break `$ref` references that point
213+
> outside the selected subtree, since only the targeted sub-schema is used for
214+
> validation. This option cannot be used together with `--template`/`-m`.

src/command_validate.cc

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -265,27 +265,30 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options)
265265
read_configuration(options, configuration_path, schema_config_base)};
266266
const auto dialect{default_dialect(options, configuration)};
267267

268-
sourcemeta::core::JSON schema{
269-
schema_from_stdin ? read_from_stdin().document
270-
: sourcemeta::core::read_yaml_or_json(schema_path)};
268+
auto schema = schema_from_stdin
269+
? read_from_stdin().document
270+
: sourcemeta::core::read_yaml_or_json(schema_path);
271271

272272
if (options.contains("path") && !options.at("path").empty()) {
273-
// Invalid pointer syntax is handled by to_pointer(), consistent with
274-
// --entrypoint behavior.
275-
const auto path_string{std::string{options.at("path").front()}};
276-
const auto pointer{sourcemeta::core::to_pointer(path_string)};
277-
const auto *const result{sourcemeta::core::try_get(schema, pointer)};
278-
// We intentionally reuse NotSchemaError here to align with existing CLI
279-
// error semantics without introducing a new error type.
273+
sourcemeta::core::Pointer pointer;
274+
try {
275+
pointer =
276+
sourcemeta::core::to_pointer(std::string{options.at("path").front()});
277+
} catch (const sourcemeta::core::PointerParseError &) {
278+
throw PositionalArgumentError{
279+
"The JSON Pointer is not valid",
280+
"jsonschema validate path/to/schema.json path/to/instance.json "
281+
"--path '/components/schemas/User'"};
282+
}
283+
284+
const auto *const result = sourcemeta::core::try_get(schema, pointer);
280285
if (!result) {
281-
throw NotSchemaError{schema_resolution_base};
286+
throw PathResolutionError{schema_resolution_base,
287+
sourcemeta::core::to_string(pointer)};
282288
}
283-
// Note: extracting a sub-schema may break $ref references outside the
284-
// selected subtree. This is expected behavior for --path given the current
285-
// CLI design.
289+
286290
// `result` points into `schema`, so we must copy before reassigning to
287-
// avoid a use-after-free (the copy assignment destroys schema's storage
288-
// before reading from other when they alias).
291+
// avoid a use-after-free.
289292
sourcemeta::core::JSON subschema{*result};
290293
schema = std::move(subschema);
291294
}

src/error.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,26 @@ class NotSchemaError : public std::runtime_error {
7676
std::filesystem::path path_;
7777
};
7878

79+
class PathResolutionError : public std::runtime_error {
80+
public:
81+
PathResolutionError(std::filesystem::path path, std::string pointer)
82+
: std::runtime_error{"The JSON Pointer does not resolve to a value in "
83+
"the document"},
84+
path_{std::move(path)}, pointer_{std::move(pointer)} {}
85+
86+
[[nodiscard]] auto path() const noexcept -> const std::filesystem::path & {
87+
return this->path_;
88+
}
89+
90+
[[nodiscard]] auto pointer() const noexcept -> const std::string & {
91+
return this->pointer_;
92+
}
93+
94+
private:
95+
std::filesystem::path path_;
96+
std::string pointer_;
97+
};
98+
7999
class YAMLInputError : public std::runtime_error {
80100
public:
81101
YAMLInputError(std::string message, std::filesystem::path path)
@@ -446,6 +466,16 @@ inline auto print_exception(const bool is_json, const Exception &exception)
446466
}
447467
}
448468

469+
if constexpr (requires(const Exception &current) {
470+
{ current.pointer() } -> std::convertible_to<std::string>;
471+
}) {
472+
if (is_json) {
473+
error_json.assign("pointer", sourcemeta::core::JSON{exception.pointer()});
474+
} else {
475+
std::cerr << " at path " << exception.pointer() << "\n";
476+
}
477+
}
478+
449479
if constexpr (requires(const Exception &current) { current.location(); }) {
450480
if (is_json) {
451481
error_json.assign("location",
@@ -560,6 +590,10 @@ inline auto try_catch(const sourcemeta::core::Options &options,
560590
const auto is_json{options.contains("json")};
561591
print_exception(is_json, error);
562592
return EXIT_OTHER_INPUT_ERROR;
593+
} catch (const PathResolutionError &error) {
594+
const auto is_json{options.contains("json")};
595+
print_exception(is_json, error);
596+
return EXIT_OTHER_INPUT_ERROR;
563597
} catch (const NotSchemaError &error) {
564598
const auto is_json{options.contains("json")};
565599
print_exception(is_json, error);

src/main.cc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Global Options:
3838
[--benchmark/-b] [--loop <iterations>] [--extension/-e <extension>]
3939
[--ignore/-i <schemas-or-directories>] [--trace/-t] [--fast/-f]
4040
[--template/-m <template.json>] [--entrypoint/-p <pointer|uri>]
41-
[--path <pointer>]
41+
[--path/-P <pointer>]
4242
4343
Validate one or more instances against the given schema.
4444
@@ -53,9 +53,8 @@ Global Options:
5353
for error reporting purposes. Make sure they match or you will get
5454
non-sense results.
5555
56-
Use --path to extract a sub-schema from the input document using
57-
a JSON Pointer before validation. This is useful for validating
58-
against schemas embedded in larger documents, such as OpenAPI.
56+
Use --path/-P to extract a sub-schema using a JSON Pointer
57+
before validation.
5958
6059
metaschema [schemas-or-directories...] [--extension/-e <extension>]
6160
[--ignore/-i <schemas-or-directories>] [--trace/-t]
@@ -187,7 +186,7 @@ auto jsonschema_main(const std::string &program, const std::string &command,
187186
app.option("template", {"m"});
188187
app.option("loop", {"l"});
189188
app.option("entrypoint", {"p"});
190-
app.option("path", {});
189+
app.option("path", {"P"});
191190
app.parse(argc, argv, {.skip = 1});
192191
sourcemeta::jsonschema::validate(app);
193192
return EXIT_SUCCESS;

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ add_jsonschema_test_unix(validate/fail_path_not_found)
247247
add_jsonschema_test_unix(validate/fail_path_not_schema)
248248
add_jsonschema_test_unix(validate/fail_path_with_template)
249249
add_jsonschema_test_unix(validate/fail_path_ref_outside_subtree)
250+
add_jsonschema_test_unix(validate/fail_path_invalid_pointer)
250251
add_jsonschema_test_unix(validate/pass_config_ignore)
251252
add_jsonschema_test_unix(validate/pass_config_ignore_with_cli)
252253
add_jsonschema_test_unix(validate/pass_stdin_instance)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/bin/sh
2+
3+
set -o errexit
4+
set -o nounset
5+
6+
TMP="$(mktemp -d)"
7+
clean() { rm -rf "$TMP"; }
8+
trap clean EXIT
9+
10+
cat << 'EOF' > "$TMP/schema.json"
11+
{
12+
"$schema": "https://json-schema.org/draft/2020-12/schema",
13+
"type": "object"
14+
}
15+
EOF
16+
17+
cat << 'EOF' > "$TMP/instance.json"
18+
{}
19+
EOF
20+
21+
"$1" validate "$TMP/schema.json" "$TMP/instance.json" \
22+
--path 'invalid~pointer' > "$TMP/output.txt" 2>&1 \
23+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
24+
# Invalid CLI arguments
25+
test "$EXIT_CODE" = "5"
26+
27+
cat << EOF > "$TMP/expected.txt"
28+
error: The JSON Pointer is not valid
29+
30+
For example: jsonschema validate path/to/schema.json path/to/instance.json --path '/components/schemas/User'
31+
EOF
32+
33+
diff "$TMP/output.txt" "$TMP/expected.txt"
34+
35+
# JSON error
36+
"$1" validate "$TMP/schema.json" "$TMP/instance.json" \
37+
--path 'invalid~pointer' --json > "$TMP/stdout.txt" 2>&1 \
38+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
39+
test "$EXIT_CODE" = "5"
40+
41+
cat << EOF > "$TMP/expected.txt"
42+
{
43+
"error": "The JSON Pointer is not valid"
44+
}
45+
EOF
46+
47+
diff "$TMP/stdout.txt" "$TMP/expected.txt"

test/validate/fail_path_not_found.sh

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,29 @@ EOF
2727
"$1" validate "$TMP/document.json" "$TMP/instance.json" \
2828
--path "/components/schemas/NonExistent" 2> "$TMP/stderr.txt" \
2929
&& EXIT_CODE="$?" || EXIT_CODE="$?"
30-
# Schema input error
31-
test "$EXIT_CODE" = "4"
30+
# Other input error (path not found)
31+
test "$EXIT_CODE" = "6"
3232

3333
cat << EOF > "$TMP/expected.txt"
34-
error: The schema file you provided does not represent a valid JSON Schema
34+
error: The JSON Pointer does not resolve to a value in the document
3535
at file path $(realpath "$TMP")/document.json
36+
at path /components/schemas/NonExistent
3637
EOF
3738

3839
diff "$TMP/stderr.txt" "$TMP/expected.txt"
40+
41+
# JSON error
42+
"$1" validate "$TMP/document.json" "$TMP/instance.json" \
43+
--path "/components/schemas/NonExistent" --json > "$TMP/stdout.txt" \
44+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
45+
test "$EXIT_CODE" = "6"
46+
47+
cat << EOF > "$TMP/expected.txt"
48+
{
49+
"error": "The JSON Pointer does not resolve to a value in the document",
50+
"filePath": "$(realpath "$TMP")/document.json",
51+
"pointer": "/components/schemas/NonExistent"
52+
}
53+
EOF
54+
55+
diff "$TMP/stdout.txt" "$TMP/expected.txt"

test/validate/fail_path_not_schema.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,18 @@ error: The schema file you provided does not represent a valid JSON Schema
3333
EOF
3434

3535
diff "$TMP/stderr.txt" "$TMP/expected.txt"
36+
37+
# JSON error
38+
"$1" validate "$TMP/document.json" "$TMP/instance.json" \
39+
--path "/components/schemas/User" --json > "$TMP/stdout.txt" \
40+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
41+
test "$EXIT_CODE" = "4"
42+
43+
cat << EOF > "$TMP/expected.txt"
44+
{
45+
"error": "The schema file you provided does not represent a valid JSON Schema",
46+
"filePath": "$(realpath "$TMP")/document.json"
47+
}
48+
EOF
49+
50+
diff "$TMP/stdout.txt" "$TMP/expected.txt"

test/validate/fail_path_ref_outside_subtree.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,4 @@ cat << EOF > "$TMP/expected.txt"
5959
}
6060
EOF
6161

62-
diff "$TMP/stdout.txt" "$TMP/expected.txt"
63-
62+
diff "$TMP/stdout.txt" "$TMP/expected.txt"

test/validate/pass_path_openapi.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,26 @@ EOF
3131

3232
"$1" validate "$TMP/openapi.json" "$TMP/instance.json" \
3333
--path "/components/schemas/User"
34+
35+
# Verbose run
36+
"$1" validate "$TMP/openapi.json" "$TMP/instance.json" \
37+
--path "/components/schemas/User" --verbose 2> "$TMP/stderr.txt"
38+
39+
cat << EOF > "$TMP/expected_verbose.txt"
40+
ok: $(realpath "$TMP")/instance.json
41+
matches $(realpath "$TMP")/openapi.json
42+
EOF
43+
44+
diff "$TMP/stderr.txt" "$TMP/expected_verbose.txt"
45+
46+
# JSON run
47+
"$1" validate "$TMP/openapi.json" "$TMP/instance.json" \
48+
--path "/components/schemas/User" --json > "$TMP/stdout.txt"
49+
50+
cat << 'EOF' > "$TMP/expected_json.txt"
51+
{
52+
"valid": true
53+
}
54+
EOF
55+
56+
diff "$TMP/stdout.txt" "$TMP/expected_json.txt"

0 commit comments

Comments
 (0)