Skip to content
Open
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# CHANGELOG

## Unreleased

* Add partial support for JSON Schema draft 2019-09 and draft 2020-12. New
dialect modules `jesse_validator_draft2019_09` and
`jesse_validator_draft2020_12`, dispatched from the schema's `$schema` URI
(`https://json-schema.org/draft/2019-09/schema` and
`.../2020-12/schema`, with or without a trailing `#`). Implemented keywords:
`dependentRequired`, `dependentSchemas`, `if`/`then`/`else`,
`minContains`/`maxContains`, `$defs`, local `$anchor` resolution, `$ref`
evaluated alongside sibling keywords, and
`unevaluatedProperties`/`unevaluatedItems` (with the full annotation
model — adjacent keywords and successful in-place applicators contribute,
cousins/uncles do not). Draft 2020-12 additionally handles the `prefixItems`
rename (tuple validation) and `items` as the after-`prefixItems` applicator.
`format` is annotation-only (non-asserting), per the dialect default.
Not yet implemented — `$recursiveRef` (2019-09) and `$dynamicRef` (2020-12) —
raise a `keyword_not_supported` schema error instead of being silently
ignored, so they can never false-accept invalid data.
Validated against the official JSON-Schema-Test-Suite: 927/1003 individual
draft 2019-09 tests and 935/997 draft 2020-12 tests pass; the remainder are
for `$recursiveRef`/`$dynamicRef`, remote-schema fetching, and in-document
`$id` scoping.

## 1.5.6

* Improving the error messages from jesse when using oneOf/anyOf
Expand Down
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ symlinks: test/JSON-Schema-Test-Suite/tests
ln -sf ../../test/JSON-Schema-Test-Suite/tests/draft6 standard && \
ln -sf ../../test/JSON-Schema-Test-Suite/remotes remotes

cd test/jesse_tests_draft2019_09_SUITE_data && \
ln -sf ../../test/JSON-Schema-Test-Suite/tests/draft2019-09 standard && \
ln -sf ../../test/JSON-Schema-Test-Suite/remotes remotes

cd test/jesse_tests_draft2020_12_SUITE_data && \
ln -sf ../../test/JSON-Schema-Test-Suite/tests/draft2020-12 standard && \
ln -sf ../../test/JSON-Schema-Test-Suite/remotes remotes

# https://github.com/erlang/rebar3/issues/2903
./rebar3.OTP18:
$(CURL) -qfsSL -o $@ https://github.com/erlang/rebar3/releases/download/3.15.3/rebar3 && $(CHMOD) +x $@
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ jesse implements the following specifications:
* [Draft 03](http://tools.ietf.org/html/draft-zyp-json-schema-03)
* [Draft 04](http://tools.ietf.org/html/draft-zyp-json-schema-04)
* [Draft 06](https://datatracker.ietf.org/doc/html/draft-wright-json-schema-00)
* [Draft 2019-09](https://json-schema.org/draft/2019-09/json-schema-core.html)
(partial: `$recursiveRef` and remote-schema fetching are not yet supported
and raise an error rather than being silently ignored)
* [Draft 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html)
(partial: `$dynamicRef` and remote-schema fetching are not yet supported
and raise an error rather than being silently ignored)

Install from git or https://hex.pm/packages/jesse .

Expand Down Expand Up @@ -241,9 +247,14 @@ Maps example

## JSON Schema versions

jesse currently supports JSON Schema draft3, draft4 and draft6. To decide which
jesse currently supports JSON Schema draft3, draft4, draft6 and (partially)
draft 2019-09 and draft 2020-12. To decide which
validator to use jesse tries to read `$schema` property from the given schema,
and checks if it's a supported one, otherwise it will return an error.
For draft 2019-09/2020-12, keywords that the dialect defines but jesse does not
yet implement (`$recursiveRef`, `$dynamicRef`) raise a `keyword_not_supported`
schema error instead of being silently ignored, so a schema relying on them can
never quietly accept data those keywords would reject.
If `$schema` property isn't provided in the given schema, jesse will use the
default validator (currently the validator for draft3).

Expand Down
7 changes: 6 additions & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@
, {elvis_style, no_behavior_info}
, { elvis_style
, module_naming_convention
, #{ regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$"
%% Allow purely-numeric underscore-delimited segments
%% (e.g. the "_09" in jesse_validator_draft2019_09) so
%% draft-year_month dialect modules can be named after
%% their metaschema version.
, #{ regex =>
"^([a-z][a-z0-9]*)(_[a-z0-9]+)*(_SUITE)?$"
, ignore => []
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/jesse.erl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
, validate/3
, validate_with_schema/2
, validate_with_schema/3
, supported_dialect/1
]).

-export_type([ allowed_errors/0
Expand Down Expand Up @@ -247,6 +248,15 @@ validate_with_schema(Schema, Data, Options) ->
throw:Error -> {error, Error}
end.

%% @doc Whether the given `$schema' dialect URI is one jesse can validate
%% against. Useful for rejecting a schema that declares an unsupported dialect
%% at registration time, instead of accepting it and failing every validation
%% at run time. Draft 2019-09/2020-12 URIs are accepted with or without a
%% trailing `#'.
-spec supported_dialect(SchemaURI :: binary()) -> boolean().
supported_dialect(SchemaURI) ->
jesse_schema_validator:is_supported_dialect(SchemaURI).

%%% Internal functions
%% @doc Wraps up calls to a third party json parser.
%% @private
Expand Down
4 changes: 4 additions & 0 deletions src/jesse_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ compare_properties(Value1, Value2) ->
get_schema_id_key(Schema) ->
case jesse_json_path:value(?SCHEMA, Schema, ?json_schema_draft6) of
?json_schema_draft6 -> ?ID;
?json_schema_draft2019_09 -> ?ID;
<<"https://json-schema.org/draft/2019-09/schema#">> -> ?ID;
?json_schema_draft2020_12 -> ?ID;
<<"https://json-schema.org/draft/2020-12/schema#">> -> ?ID;
_ -> ?ID_OLD
end.

Expand Down
50 changes: 48 additions & 2 deletions src/jesse_schema_validator.erl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
%% API
-export([ validate/3
, validate_with_state/3
, is_supported_dialect/1
]).

%% Includes
Expand Down Expand Up @@ -57,6 +58,23 @@ validate_with_state(JsonSchema, Value, State) ->
SchemaVer = get_schema_ver(JsonSchema, State),
select_and_run_validator(SchemaVer, JsonSchema, Value, State).

%% @doc Whether a schema's `$schema' dialect URI is one jesse can validate
%% against. Intended for callers that want to reject a schema declaring an
%% unsupported dialect up-front (e.g. at registration) instead of having every
%% validation fail at run time. Draft 2019-09/2020-12 URIs are accepted with or
%% without a trailing `#'.
-spec is_supported_dialect(SchemaURI :: binary()) -> boolean().
is_supported_dialect(?json_schema_draft3) -> true;
is_supported_dialect(?json_schema_draft4) -> true;
is_supported_dialect(?json_schema_draft6) -> true;
is_supported_dialect(SchemaURI) when is_binary(SchemaURI) ->
case normalize_schema_ver(SchemaURI) of
?json_schema_draft2019_09 -> true;
?json_schema_draft2020_12 -> true;
_ -> false
end;
is_supported_dialect(_) -> false.

%%% Internal functions
%% @doc Returns "$schema" property from `JsonSchema' if it is present,
%% otherwise the default schema version from `State' is returned.
Expand Down Expand Up @@ -94,5 +112,33 @@ select_and_run_validator(?json_schema_draft6, JsonSchema, Value, State) ->
, jesse_json_path:unwrap_value(JsonSchema)
, State
);
select_and_run_validator(SchemaURI, _JsonSchema, _Value, State) ->
jesse_error:handle_schema_invalid({?schema_unsupported, SchemaURI}, State).
select_and_run_validator(SchemaURI, JsonSchema, Value, State) ->
case normalize_schema_ver(SchemaURI) of
?json_schema_draft2019_09 ->
jesse_validator_draft2019_09:check_value(
Value, jesse_json_path:unwrap_value(JsonSchema), State);
?json_schema_draft2020_12 ->
jesse_validator_draft2020_12:check_value(
Value, jesse_json_path:unwrap_value(JsonSchema), State);
_ ->
jesse_error:handle_schema_invalid({?schema_unsupported, SchemaURI}, State)
end.

%% @doc Normalize a "$schema" URI so that draft 2019-09/2020-12 schemas dispatch
%% regardless of a trailing "#" fragment or http/https scheme. Draft 3/4/6 are
%% matched verbatim by the clauses above and never reach here.
%% @private
normalize_schema_ver(SchemaURI) when is_binary(SchemaURI) ->
Stripped =
case SchemaURI of
<<Base:(byte_size(SchemaURI) - 1)/binary, $#>> -> Base;
_ -> SchemaURI
end,
case Stripped of
<<"http://json-schema.org/", Rest/binary>> ->
<<"https://json-schema.org/", Rest/binary>>;
_ ->
Stripped
end;
normalize_schema_ver(SchemaURI) ->
SchemaURI.
32 changes: 32 additions & 0 deletions src/jesse_schema_validator.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@
-define(MAXPROPERTIES, <<"maxProperties">>).
-define(MINPROPERTIES, <<"minProperties">>).

%% Keywords introduced in draft 2019-09 / 2020-12
-define(DEFS, <<"$defs">>).
-define(ANCHOR, <<"$anchor">>).
-define(RECURSIVEANCHOR, <<"$recursiveAnchor">>).
-define(RECURSIVEREF, <<"$recursiveRef">>).
-define(DYNAMICANCHOR, <<"$dynamicAnchor">>).
-define(DYNAMICREF, <<"$dynamicRef">>).
-define(VOCABULARY, <<"$vocabulary">>).
-define(COMMENT, <<"$comment">>).
-define(DEPENDENTREQUIRED, <<"dependentRequired">>).
-define(DEPENDENTSCHEMAS, <<"dependentSchemas">>).
-define(IF, <<"if">>).
-define(THEN, <<"then">>).
-define(ELSE, <<"else">>).
-define(MINCONTAINS, <<"minContains">>).
-define(MAXCONTAINS, <<"maxContains">>).
-define(PREFIXITEMS, <<"prefixItems">>).
-define(UNEVALUATEDPROPERTIES, <<"unevaluatedProperties">>).
-define(UNEVALUATEDITEMS, <<"unevaluatedItems">>).

%% Constant definitions for Json types
-define(ANY, <<"any">>).
-define(ARRAY, <<"array">>).
Expand All @@ -96,6 +116,14 @@
-define(json_schema_draft3, <<"http://json-schema.org/draft-03/schema#">>).
-define(json_schema_draft4, <<"http://json-schema.org/draft-04/schema#">>).
-define(json_schema_draft6, <<"http://json-schema.org/draft-06/schema#">>).
%% Draft 2019-09 and 2020-12 canonical metaschema URIs omit the trailing "#".
%% Incoming "$schema" values are normalized (trailing "#" stripped, scheme
%% coerced to https) before being matched against these, so both the
%% fragment and non-fragment forms dispatch to the right dialect.
-define(json_schema_draft2019_09,
<<"https://json-schema.org/draft/2019-09/schema">>).
-define(json_schema_draft2020_12,
<<"https://json-schema.org/draft/2020-12/schema">>).
-define(default_schema_ver, ?json_schema_draft6).
-define(default_schema_loader_fun, fun jesse_database:load_uri/1).
-define(default_error_handler_fun, fun jesse_error:default_error_handler/3).
Expand All @@ -107,6 +135,10 @@
-define(schema_invalid, 'schema_invalid').
-define(schema_not_found, 'schema_not_found').
-define(schema_unsupported, 'schema_unsupported').
%% Raised when a schema uses a keyword that the dialect defines but jesse does
%% not yet implement. Surfacing it (instead of silently ignoring the keyword)
%% prevents dangerous false-acceptance of data the keyword would have rejected.
-define(keyword_not_supported, 'keyword_not_supported').
-define(wrong_all_of_schema_array, 'wrong_all_of_schema_array').
-define(wrong_any_of_schema_array, 'wrong_any_of_schema_array').
-define(wrong_max_properties, 'wrong_max_properties').
Expand Down
Loading