Author SHA1 Message Date
Ulf Wiger 25f81ae339 Add JSON Schema draft discrimination
Track the declared JSON Schema dialect in the validator state and validate
dialect-sensitive constructs before applying a schema.

Recognize draft-04, draft-06, draft-07, 2019-09, and 2020-12, while preserving
the existing compatibility behavior for schemas without a $schema declaration.
Reject unknown dialects and known keywords or forms that do not belong to the
declared draft.

Implement draft-specific exclusive bound semantics, boolean-schema rules, and
array vocabulary differences. Document the behavior and add coverage for all
supported drafts.

The generated Gajumaru draft-04 configuration schema passes the new checks.
2026-08-29 11:30:01 +02:00
uwiger 73944804c1 Merge pull request 'Clarify validation example in README' (#6) from uw-clarify-readme into master
Reviewed-on: #6
2026-05-14 19:10:20 +09:00
Ulf Wiger ffa189b885 Clarify validation example in README 2026-05-14 11:32:17 +02:00
uwigerandUlf Wiger 08287da7b7 Validator extensions, zomp vsn 0.2.0 (#5)
Co-authored-by: Ulf Wiger <ulf@wiger.net>
Reviewed-on: #5
2026-05-14 17:00:02 +09:00
3 changed files with 502 additions and 15 deletions
+34 -5
View File
@@ -25,6 +25,19 @@ is still incomplete. `"definitions"` and `"$ref"` properties are recognized.
As almost anything is theoretically possible with JSON-Schema,
there are surely other things that are unsupported.
### Schema dialects
If a schema declares `"$schema"`, the validator recognizes draft-04,
draft-06, draft-07, 2019-09 and 2020-12. It rejects unknown dialects and
known constructs that do not belong to the declared draft. This currently
includes boolean-schema availability, keyword introduction dates,
draft-specific exclusive-bound syntax and the 2020-12 array vocabulary.
Schemas without `"$schema"` retain the historical compatibility behavior,
which accepts the supported keyword subset without assigning it to one
particular JSON-Schema draft. Dialect recognition does not yet imply full
conformance with every keyword in that draft.
### All standard data types
* `"null"`
@@ -115,13 +128,29 @@ given string value.
In the test schema, we can see the following definition:
```json
"Pubkey": {
"type": "string",
"x-serialization": {
"tags": ["ak", "ct"]
}
"properties": {
"from": {
"allOf": [
{ "$ref": "#/components/schemas/Pubkey" },
{ "x-serialization": {
"tags": ["ak"]
}}
]
}
}
...
"Pubkey": {
"type": "string",
"x-serialization": {
"tags": ["ak", "ct"]
}
```
Whenever the validator encounters an `x-...` property mapped to a validator fun,
this fun is called with the value and the schema part of the property. The return
value of the fun is ignored, and any normal return is treated as a validation success.
The example illustrates a common pattern in OpenAPI specs, where entity references are
used extensively. The `Pubkey` data type can have a more general `x-serialization`
definition, where multiple key types are accepted, whereas a specialized use of the
type can narrow the scope by accepting only a subset of the possible types.
+318 -10
View File
@@ -46,6 +46,13 @@
-type schema() :: json().
-type draft() :: compatibility
| draft4
| draft6
| draft7
| draft2019_09
| draft2020_12.
-type ext_fun() :: fun( (json(), schema()) -> any() | no_return() ).
-type extensions() :: #{ binary() => ext_fun() }.
-type options() :: #{coerce => boolean(),
@@ -54,6 +61,7 @@
-record(st, { s :: schema() %% schema
, r :: schema() %% root schema
, draft = compatibility :: draft()
, p = []
, a = [] %% annotations
, v :: json() | undefined %% value
@@ -89,10 +97,10 @@ get_schema(Default) ->
-spec use_schema(schema() | st()) -> st().
use_schema(#st{} = St) -> St;
use_schema(S) -> #st{s = S, r = S}.
use_schema(S) -> new_state(S, S).
use_schema(Schema, RootSchema) ->
#st{s = Schema, r = RootSchema}.
new_state(Schema, RootSchema).
normalize() ->
normalize(get_schema()).
@@ -107,6 +115,293 @@ normalize(S) ->
bin_key(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin_key(B) when is_binary(B) -> B.
new_state(Schema, RootSchema) ->
Draft = schema_draft(RootSchema, compatibility, []),
case Draft of
compatibility -> ok;
_ -> check_schema(normalize(RootSchema), Draft, [])
end,
#st{s = Schema, r = RootSchema, draft = Draft}.
schema_draft(Schema, Inherited, Path) when is_map(Schema) ->
case schema_value(<<"$schema">>, '$schema', Schema) of
undefined ->
Inherited;
URI ->
draft_from_uri(URI, Path ++ [<<"$schema">>])
end;
schema_draft(_, Inherited, _) ->
Inherited.
schema_value(BinaryKey, AtomKey, Schema) ->
case maps:find(BinaryKey, Schema) of
{ok, Value} -> Value;
error -> maps:get(AtomKey, Schema, undefined)
end.
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft-04/schema#">>;
URI =:= <<"https://json-schema.org/draft-04/schema#">>;
URI =:= <<"http://json-schema.org/draft-04/schema">>;
URI =:= <<"https://json-schema.org/draft-04/schema">> ->
draft4;
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft-06/schema#">>;
URI =:= <<"https://json-schema.org/draft-06/schema#">>;
URI =:= <<"http://json-schema.org/draft-06/schema">>;
URI =:= <<"https://json-schema.org/draft-06/schema">> ->
draft6;
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft-07/schema#">>;
URI =:= <<"https://json-schema.org/draft-07/schema#">>;
URI =:= <<"http://json-schema.org/draft-07/schema">>;
URI =:= <<"https://json-schema.org/draft-07/schema">> ->
draft7;
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft/2019-09/schema#">>;
URI =:= <<"https://json-schema.org/draft/2019-09/schema#">>;
URI =:= <<"http://json-schema.org/draft/2019-09/schema">>;
URI =:= <<"https://json-schema.org/draft/2019-09/schema">> ->
draft2019_09;
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft/2020-12/schema#">>;
URI =:= <<"https://json-schema.org/draft/2020-12/schema#">>;
URI =:= <<"http://json-schema.org/draft/2020-12/schema">>;
URI =:= <<"https://json-schema.org/draft/2020-12/schema">> ->
draft2020_12;
draft_from_uri(URI, Path) ->
schema_error(unsupported_schema_draft,
#{draft_uri => URI, p => Path}).
check_schema(_, compatibility, _) ->
ok;
check_schema(Schema, draft4 = Draft, Path) when is_boolean(Schema) ->
case lists:reverse(Path) of
[<<"additionalProperties">> | _] -> ok;
[<<"additionalItems">> | _] -> ok;
_ ->
schema_error(boolean_schema_not_in_draft,
#{draft => Draft, p => Path})
end;
check_schema(Schema, _Draft, _Path) when is_boolean(Schema) ->
ok;
check_schema(Schema, Draft0, Path) when is_map(Schema) ->
Draft = schema_draft(Schema, Draft0, Path),
maps:foreach(
fun(Keyword0, Value) ->
Keyword = bin_key(Keyword0),
check_keyword(Keyword, Value, Draft, Path ++ [Keyword])
end, Schema),
check_schema_shape(Schema, Draft, Path),
check_subschemas(Schema, Draft, Path);
check_schema(_, Draft, Path) ->
schema_error(invalid_schema,
#{draft => Draft, p => Path}).
check_keyword(Keyword, Value, Draft, Path) ->
case minimum_draft(Keyword) of
undefined -> ok;
Minimum ->
case draft_rank(Draft) >= draft_rank(Minimum) of
true -> ok;
false ->
schema_error(keyword_not_in_draft,
#{draft => Draft,
keyword => Keyword,
introduced_in => Minimum,
p => Path})
end
end,
case maximum_draft(Keyword) of
undefined -> ok;
Maximum ->
case draft_rank(Draft) =< draft_rank(Maximum) of
true -> ok;
false ->
schema_error(keyword_not_in_draft,
#{draft => Draft,
keyword => Keyword,
last_supported_in => Maximum,
p => Path})
end
end,
check_keyword_shape(Keyword, Value, Draft, Path).
minimum_draft(<<"$id">>) -> draft6;
minimum_draft(<<"const">>) -> draft6;
minimum_draft(<<"contains">>) -> draft6;
minimum_draft(<<"propertyNames">>) -> draft6;
minimum_draft(<<"examples">>) -> draft6;
minimum_draft(<<"$comment">>) -> draft7;
minimum_draft(<<"if">>) -> draft7;
minimum_draft(<<"then">>) -> draft7;
minimum_draft(<<"else">>) -> draft7;
minimum_draft(<<"writeOnly">>) -> draft7;
minimum_draft(<<"contentMediaType">>) -> draft7;
minimum_draft(<<"contentEncoding">>) -> draft7;
minimum_draft(<<"$defs">>) -> draft2019_09;
minimum_draft(<<"$anchor">>) -> draft2019_09;
minimum_draft(<<"$recursiveAnchor">>) -> draft2019_09;
minimum_draft(<<"$recursiveRef">>) -> draft2019_09;
minimum_draft(<<"dependentRequired">>) -> draft2019_09;
minimum_draft(<<"dependentSchemas">>) -> draft2019_09;
minimum_draft(<<"unevaluatedItems">>) -> draft2019_09;
minimum_draft(<<"unevaluatedProperties">>) -> draft2019_09;
minimum_draft(<<"minContains">>) -> draft2019_09;
minimum_draft(<<"maxContains">>) -> draft2019_09;
minimum_draft(<<"contentSchema">>) -> draft2019_09;
minimum_draft(<<"$dynamicAnchor">>) -> draft2020_12;
minimum_draft(<<"$dynamicRef">>) -> draft2020_12;
minimum_draft(<<"prefixItems">>) -> draft2020_12;
minimum_draft(_) -> undefined.
maximum_draft(<<"additionalItems">>) -> draft2019_09;
maximum_draft(_) -> undefined.
draft_rank(draft4) -> 4;
draft_rank(draft6) -> 6;
draft_rank(draft7) -> 7;
draft_rank(draft2019_09) -> 8;
draft_rank(draft2020_12) -> 9.
check_keyword_shape(<<"exclusiveMinimum">>, Value, draft4, Path) ->
require_type(fun is_boolean/1, Value, draft4, Path);
check_keyword_shape(<<"exclusiveMaximum">>, Value, draft4, Path) ->
require_type(fun is_boolean/1, Value, draft4, Path);
check_keyword_shape(<<"exclusiveMinimum">>, Value, Draft, Path) ->
require_type(fun is_number/1, Value, Draft, Path);
check_keyword_shape(<<"exclusiveMaximum">>, Value, Draft, Path) ->
require_type(fun is_number/1, Value, Draft, Path);
check_keyword_shape(<<"items">>, Value, draft2020_12, Path)
when is_list(Value) ->
schema_error(keyword_value_not_in_draft,
#{draft => draft2020_12,
keyword => <<"items">>,
p => Path});
check_keyword_shape(_, _, _, _) ->
ok.
check_schema_shape(Schema, draft4, Path) ->
check_exclusive_bound(Schema, <<"exclusiveMinimum">>, <<"minimum">>,
draft4, Path),
check_exclusive_bound(Schema, <<"exclusiveMaximum">>, <<"maximum">>,
draft4, Path);
check_schema_shape(_, _, _) ->
ok.
check_exclusive_bound(Schema, Exclusive, Bound, Draft, Path) ->
case {maps:find(Exclusive, Schema), maps:is_key(Bound, Schema)} of
{{ok, _}, false} ->
schema_error(missing_keyword_for_draft,
#{draft => Draft,
keyword => Exclusive,
required_keyword => Bound,
p => Path ++ [Exclusive]});
_ ->
ok
end.
require_type(Predicate, Value, Draft, Path) ->
case Predicate(Value) of
true -> ok;
false ->
schema_error(keyword_value_not_in_draft,
#{draft => Draft, p => Path, value => Value})
end.
check_subschemas(Schema, Draft, Path) ->
check_schema_values(
[<<"additionalProperties">>, <<"additionalItems">>, <<"contains">>,
<<"propertyNames">>, <<"not">>, <<"if">>, <<"then">>, <<"else">>,
<<"unevaluatedProperties">>, <<"unevaluatedItems">>,
<<"contentSchema">>],
Schema, Draft, Path),
check_schema_arrays(
[<<"allOf">>, <<"anyOf">>, <<"oneOf">>, <<"prefixItems">>],
Schema, Draft, Path),
check_schema_maps(
[<<"properties">>, <<"patternProperties">>, <<"definitions">>,
<<"$defs">>, <<"dependentSchemas">>],
Schema, Draft, Path),
check_schema_items(Schema, Draft, Path),
check_dependencies(Schema, Draft, Path).
check_schema_values(Keys, Schema, Draft, Path) ->
lists:foreach(
fun(Key) ->
case maps:find(Key, Schema) of
{ok, SubSchema} ->
check_schema(SubSchema, Draft, Path ++ [Key]);
error ->
ok
end
end, Keys).
check_schema_arrays(Keys, Schema, Draft, Path) ->
lists:foreach(
fun(Key) ->
case maps:find(Key, Schema) of
{ok, SubSchemas} when is_list(SubSchemas) ->
check_schema_list(SubSchemas, Draft, Path ++ [Key], 0);
{ok, Value} ->
schema_error(invalid_schema,
#{draft => Draft,
keyword => Key,
value => Value,
p => Path ++ [Key]});
error ->
ok
end
end, Keys).
check_schema_list([Schema | Schemas], Draft, Path, Index) ->
check_schema(Schema, Draft, Path ++ [Index]),
check_schema_list(Schemas, Draft, Path, Index + 1);
check_schema_list([], _, _, _) ->
ok.
check_schema_maps(Keys, Schema, Draft, Path) ->
lists:foreach(
fun(Key) ->
case maps:find(Key, Schema) of
{ok, SubSchemas} when is_map(SubSchemas) ->
maps:foreach(
fun(Name, SubSchema) ->
check_schema(SubSchema, Draft,
Path ++ [Key, Name])
end, SubSchemas);
{ok, Value} ->
schema_error(invalid_schema,
#{draft => Draft,
keyword => Key,
value => Value,
p => Path ++ [Key]});
error ->
ok
end
end, Keys).
check_schema_items(Schema, Draft, Path) ->
case maps:find(<<"items">>, Schema) of
{ok, Items} when is_list(Items) ->
check_schema_list(Items, Draft, Path ++ [<<"items">>], 0);
{ok, Items} ->
check_schema(Items, Draft, Path ++ [<<"items">>]);
error ->
ok
end.
check_dependencies(Schema, Draft, Path) ->
case maps:find(<<"dependencies">>, Schema) of
{ok, Dependencies} when is_map(Dependencies) ->
maps:foreach(
fun(_, Dependency) when is_list(Dependency) -> ok;
(Name, Dependency) ->
check_schema(Dependency, Draft,
Path ++ [<<"dependencies">>, Name])
end, Dependencies);
_ ->
ok
end.
schema_error(Reason, Details) ->
error(Details#{e => Reason}).
clear() ->
persistent_term:erase({?MODULE,'$schema'}),
persistent_term:erase({?MODULE,'$config'}),
@@ -116,8 +411,7 @@ clear() ->
update_config(Cfg) ->
OldCfg = get_config(),
Schema = get_schema(),
Res = merge(Cfg, OldCfg, #st{s = Schema,
r = Schema }),
Res = merge(Cfg, OldCfg, Schema),
set_config(Res),
Res.
@@ -129,7 +423,7 @@ merge(A, B) ->
merge(A, B, #st{} = St) ->
merge_(A, B, St);
merge(A, B, Schema) ->
merge_(A, B, #st{s = Schema, r = Schema}).
merge_(A, B, new_state(Schema, Schema)).
%% Neither the JSON spec or the JSON-Schema spec are very helpful
%% regarding what takes precedence if dynamically evaluated parts
@@ -396,7 +690,7 @@ valid(V) ->
valid(V, #st{} = St) ->
valid_(V, St#st{v = V});
valid(V, Schema) ->
valid(V, #st{p = [], s = Schema, r = Schema, v = V}).
valid(V, (new_state(Schema, Schema))#st{v = V}).
valid_(V, #st{s = true}) -> V;
valid_(_, #st{s = false} = St) -> fail(invalid, St);
@@ -571,6 +865,19 @@ valid_number_(I, Sub, St, Ss) when is_number(I) ->
fail(not_a_multiple, St1)
end
end,
validate_number_bounds(I, St, Ss),
I.
validate_number_bounds(I, #st{draft = draft4} = St, Ss) ->
Min = schema_prop(<<"minimum">>, St, Ss, I),
Max = schema_prop(<<"maximum">>, St, Ss, I),
EMin = schema_prop(<<"exclusiveMinimum">>, St, Ss, false),
EMax = schema_prop(<<"exclusiveMaximum">>, St, Ss, false),
MinOp = case EMin of true -> '<'; false -> '=<' end,
MaxOp = case EMax of true -> '>'; false -> '>=' end,
test_range(MaxOp, Max, I, add_anno(Max, push_path(maximum, St))),
test_range(MinOp, Min, I, add_anno(Min, push_path(minimum, St)));
validate_number_bounds(I, St, Ss) ->
Min = schema_prop(<<"minimum">>, St, Ss, I),
Max = schema_prop(<<"maximum">>, St, Ss, I),
test_range('>=', Max, I, add_anno(Max, push_path(maximum, St))),
@@ -578,8 +885,7 @@ valid_number_(I, Sub, St, Ss) when is_number(I) ->
EMin = schema_prop(<<"exclusiveMinimum">>, St, Ss, I-1),
EMax = schema_prop(<<"exclusiveMaximum">>, St, Ss, I+1),
test_range('>', EMax, I, add_anno(EMax, push_path(exclusiveMaximum, St))),
test_range('<', EMin, I, add_anno(EMin, push_path(exclusiveMinimum, St))),
I.
test_range('<', EMin, I, add_anno(EMin, push_path(exclusiveMinimum, St))).
coerce_num(integer, I) when is_binary(I) ->
binary_to_integer(I);
@@ -872,7 +1178,8 @@ acc_props(Ss) ->
end, #{}, Ss).
s(S, #st{} = St) ->
St#st{s = S, d = undefined}.
Draft = schema_draft(S, St#st.draft, lists:reverse(St#st.p)),
St#st{s = S, d = undefined, draft = Draft}.
push_path(Ps, #st{p = P0} = St) when is_list(Ps) ->
%% Assume Ps is in reverse order
@@ -1004,7 +1311,8 @@ schema(Path, Schema) ->
schema(Path, #st{s = Schema, r = RootSchema}, Opts) ->
schema_(Path, Schema, RootSchema, Opts);
schema(Path, Schema, Opts) ->
schema_(Path, Schema, Schema, Opts).
St = new_state(Schema, Schema),
schema(Path, St, Opts).
schema_([H|T], Schema, RootSchema0, Opts) ->
RootSchema = set_rootschema(Schema, RootSchema0),
+150
View File
@@ -43,6 +43,146 @@ schema_spec_examples_test_() ->
, ?t(t_nested_refs())
]}.
draft_discrimination_test_() ->
{"JSON Schema draft discrimination",
[
?t(t_draft4_exclusive_bounds())
, ?t(t_draft4_rejects_boolean_schema())
, ?t(t_draft4_rejects_const())
, ?t(t_draft6_accepts_const())
, ?t(t_draft6_rejects_conditionals())
, ?t(t_draft7_accepts_conditionals())
, ?t(t_draft7_rejects_prefix_items())
, ?t(t_draft2020_accepts_prefix_items())
, ?t(t_draft2020_rejects_tuple_items())
, ?t(t_unspecified_draft_remains_compatible())
, ?t(t_unknown_draft_is_rejected())
]}.
-define(DRAFT4, <<"http://json-schema.org/draft-04/schema#">>).
-define(DRAFT6, <<"http://json-schema.org/draft-06/schema#">>).
-define(DRAFT7, <<"http://json-schema.org/draft-07/schema#">>).
-define(DRAFT2020, <<"https://json-schema.org/draft/2020-12/schema">>).
with_draft(Draft, Schema) ->
Schema#{<<"$schema">> => Draft}.
t_draft4_exclusive_bounds() ->
S = with_draft(?DRAFT4,
#{<<"type">> => <<"number">>,
<<"minimum">> => 1,
<<"exclusiveMinimum">> => true,
<<"maximum">> => 3,
<<"exclusiveMaximum">> => false}),
all_valid([2, 3], S),
all_fail([1, 4], S, not_in_range),
schema_fails(
with_draft(?DRAFT4,
#{<<"type">> => <<"number">>,
<<"exclusiveMinimum">> => 1}),
#{e => keyword_value_not_in_draft,
draft => draft4,
p => [<<"exclusiveMinimum">>]}),
schema_fails(
with_draft(?DRAFT4,
#{<<"type">> => <<"number">>,
<<"exclusiveMinimum">> => true}),
#{e => missing_keyword_for_draft,
draft => draft4,
keyword => <<"exclusiveMinimum">>,
required_keyword => <<"minimum">>}).
t_draft4_rejects_boolean_schema() ->
S = with_draft(?DRAFT4,
#{<<"properties">> => #{<<"disabled">> => false}}),
schema_fails(S, #{e => boolean_schema_not_in_draft,
draft => draft4,
p => [<<"properties">>, <<"disabled">>]}),
ObjectSchema = with_draft(?DRAFT4,
#{<<"type">> => <<"object">>,
<<"additionalProperties">> => false}),
is_valid(#{}, ObjectSchema),
fails(#{<<"unexpected">> => true}, ObjectSchema, invalid).
t_draft4_rejects_const() ->
S = with_draft(?DRAFT4,
#{<<"properties">> =>
#{<<"mode">> => #{<<"const">> => <<"on">>}}}),
schema_fails(S, #{e => keyword_not_in_draft,
draft => draft4,
keyword => <<"const">>,
introduced_in => draft6,
p => [<<"properties">>, <<"mode">>, <<"const">>]}).
t_draft6_accepts_const() ->
S = with_draft(?DRAFT6, #{<<"const">> => <<"on">>}),
is_valid(<<"on">>, S),
fails(<<"off">>, S, not_in_enum).
t_draft6_rejects_conditionals() ->
S = with_draft(?DRAFT6,
#{<<"if">> => #{<<"const">> => 1},
<<"then">> => false}),
schema_fails(S, #{e => keyword_not_in_draft,
draft => draft6,
keyword => <<"if">>,
introduced_in => draft7,
p => [<<"if">>]}).
t_draft7_accepts_conditionals() ->
S = with_draft(?DRAFT7,
#{<<"if">> => #{<<"const">> => 1},
<<"then">> => #{<<"minimum">> => 1},
<<"else">> => #{<<"minimum">> => 2}}),
all_valid([1, 2], S),
fails(0, S, not_in_range).
t_draft7_rejects_prefix_items() ->
S = with_draft(?DRAFT7,
#{<<"type">> => <<"array">>,
<<"prefixItems">> => [#{<<"type">> => <<"integer">>}]}),
schema_fails(S, #{e => keyword_not_in_draft,
draft => draft7,
keyword => <<"prefixItems">>,
introduced_in => draft2020_12,
p => [<<"prefixItems">>]}).
t_draft2020_accepts_prefix_items() ->
S = with_draft(?DRAFT2020,
#{<<"type">> => <<"array">>,
<<"prefixItems">> => [#{<<"type">> => <<"integer">>}],
<<"items">> => false}),
is_valid([1], S),
fails([1, 2], S, invalid).
t_draft2020_rejects_tuple_items() ->
S = with_draft(?DRAFT2020,
#{<<"type">> => <<"array">>,
<<"items">> => [#{<<"type">> => <<"integer">>}]}),
schema_fails(S, #{e => keyword_value_not_in_draft,
draft => draft2020_12,
keyword => <<"items">>,
p => [<<"items">>]}),
Additional = with_draft(?DRAFT2020,
#{<<"type">> => <<"array">>,
<<"additionalItems">> => false}),
schema_fails(Additional, #{e => keyword_not_in_draft,
draft => draft2020_12,
keyword => <<"additionalItems">>,
last_supported_in => draft2019_09,
p => [<<"additionalItems">>]}).
t_unspecified_draft_remains_compatible() ->
S = #{<<"type">> => <<"array">>,
<<"prefixItems">> => [#{<<"type">> => <<"integer">>}]},
is_valid([1], S).
t_unknown_draft_is_rejected() ->
S = with_draft(<<"https://example.test/schema-dialect">>, #{}),
schema_fails(S, #{e => unsupported_schema_draft,
draft_uri => <<"https://example.test/schema-dialect">>,
p => [<<"$schema">>]}).
array() -> #{<<"type">> => <<"array">>}.
int() -> #{<<"type">> => <<"integer">>}.
str() -> #{<<"type">> => <<"string">>}.
@@ -303,6 +443,16 @@ fails(V, S, Opts, Expect) ->
end.
%% ?assertError({Reason, [], V}, valid(V, S)).
schema_fails(Schema, Expect) ->
try valid(null, Schema) of
_ ->
error({expected_schema_exception,
#{s => Schema, e => Expect}})
catch
error:Reason ->
match_expected(Expect, Reason)
end.
match_expected(E, R) ->
case maps:fold(
fun(K, V, Acc) ->