File Coverage

blib/lib/JSON/Schema/Modern.pm
Criterion Covered Total %
statement 351 354 99.1
branch 127 150 84.6
condition 78 90 86.6
subroutine 51 51 100.0
pod 7 9 77.7
total 614 654 93.8


line stmt bran cond sub pod time code
1 34     34   10867746 use strict;
  34         414  
  34         1090  
2 34     34   203 use warnings;
  34         101  
  34         2023  
3             package JSON::Schema::Modern; # git description: v0.570-9-gb5589c0e
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Validate data against a schema
6             # KEYWORDS: JSON Schema validator data validation structure specification
7              
8             our $VERSION = '0.571';
9              
10 34     34   952 use 5.020; # for fc, unicode_strings features
  34         129  
11 34     34   13724 use Moo;
  34         177215  
  34         254  
12 34     34   43304 use strictures 2;
  34         479  
  34         1709  
13 34     34   7972 use stable 0.031 'postderef';
  34         753  
  34         349  
14 34     34   6788 use experimental 'signatures';
  34         79  
  34         170  
15 34     34   2948 use if "$]" >= 5.022, experimental => 're_strict';
  34         134  
  34         397  
16 34     34   3603 no if "$]" >= 5.031009, feature => 'indirect';
  34         110  
  34         411  
17 34     34   1836 no if "$]" >= 5.033001, feature => 'multidimensional';
  34         205  
  34         264  
18 34     34   1812 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  34         97  
  34         337  
19 34     34   13445 use JSON::MaybeXS;
  34         142342  
  34         2289  
20 34     34   250 use Carp qw(croak carp);
  34         85  
  34         2156  
21 34     34   243 use List::Util 1.55 qw(pairs first uniqint pairmap uniq any);
  34         780  
  34         3560  
22 34     34   12329 use Ref::Util 0.100 qw(is_ref is_plain_hashref);
  34         41475  
  34         2581  
23 34     34   265 use Scalar::Util 'refaddr';
  34         107  
  34         1627  
24 34     34   17607 use Mojo::URL;
  34         6425576  
  34         274  
25 34     34   12682 use Safe::Isa;
  34         11507  
  34         4909  
26 34     34   21779 use Path::Tiny;
  34         321248  
  34         2215  
27 34     34   24164 use Storable 'dclone';
  34         116336  
  34         2544  
28 34     34   13098 use File::ShareDir 'dist_dir';
  34         686234  
  34         2153  
29 34     34   12731 use Module::Runtime qw(use_module require_module);
  34         44721  
  34         933  
30 34     34   12706 use MooX::TypeTiny 0.002002;
  34         8866  
  34         237  
31 34     34   204630 use MooX::HandlesVia;
  34         27296  
  34         237  
32 34     34   20567 use Types::Standard 1.016003 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict CodeRef Optional Slurpy ArrayRef Undef ClassName Tuple Map);
  34         2853673  
  34         531  
33 34     34   182758 use Feature::Compat::Try;
  34         8909  
  34         290  
34 34     34   89637 use JSON::Schema::Modern::Error;
  34         143  
  34         1582  
35 34     34   19481 use JSON::Schema::Modern::Result;
  34         159  
  34         1564  
36 34     34   17885 use JSON::Schema::Modern::Document;
  34         176  
  34         500  
37 34     34   20435 use JSON::Schema::Modern::Utilities qw(get_type canonical_uri E abort annotate_self);
  34         109  
  34         3449  
38 34     34   289 use namespace::clean;
  34         94  
  34         224  
39              
40             our @CARP_NOT = qw(
41             JSON::Schema::Modern::Document
42             JSON::Schema::Modern::Vocabulary
43             JSON::Schema::Modern::Vocabulary::Applicator
44             OpenAPI::Modern
45             );
46              
47 34     34   23906 use constant SPECIFICATION_VERSION_DEFAULT => 'draft2020-12';
  34         96  
  34         2200  
48 34     34   248 use constant SPECIFICATION_VERSIONS_SUPPORTED => [qw(draft7 draft2019-09 draft2020-12)];
  34         85  
  34         225944  
49              
50             has specification_version => (
51             is => 'ro',
52             isa => Enum(SPECIFICATION_VERSIONS_SUPPORTED),
53             coerce => sub {
54             return $_[0] if any { $_[0] eq $_ } SPECIFICATION_VERSIONS_SUPPORTED->@*;
55             my $real = 'draft'.($_[0]//'');
56             (any { $real eq $_ } SPECIFICATION_VERSIONS_SUPPORTED->@*) ? $real : $_[0];
57             },
58             );
59              
60             has output_format => (
61             is => 'ro',
62             isa => Enum(JSON::Schema::Modern::Result->OUTPUT_FORMATS),
63             default => 'basic',
64             );
65              
66             has short_circuit => (
67             is => 'ro',
68             isa => Bool,
69             lazy => 1,
70             default => sub { $_[0]->output_format eq 'flag' && !$_[0]->collect_annotations },
71             );
72              
73             has max_traversal_depth => (
74             is => 'ro',
75             isa => Int,
76             default => 50,
77             );
78              
79             has validate_formats => (
80             is => 'ro',
81             isa => Bool,
82             lazy => 1,
83             # as specified by https://json-schema.org/draft/<version>/schema#/$vocabulary
84             default => sub { ($_[0]->specification_version//SPECIFICATION_VERSION_DEFAULT) eq 'draft7' ? 1 : 0 },
85             );
86              
87             has validate_content_schemas => (
88             is => 'ro',
89             isa => Bool,
90             lazy => 1,
91             # defaults to false in latest versions, as specified by
92             # https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.8.2
93             default => sub { ($_[0]->specification_version//'') eq 'draft7' },
94             );
95              
96             has [qw(collect_annotations scalarref_booleans stringy_numbers strict)] => (
97             is => 'ro',
98             isa => Bool,
99             );
100              
101             has _format_validations => (
102             is => 'bare',
103             isa => my $format_type = Dict[
104             (map +($_ => Optional[CodeRef]), qw(date-time date time duration email idn-email hostname idn-hostname ipv4 ipv6 uri uri-reference iri iri-reference uuid uri-template json-pointer relative-json-pointer regex)),
105             Slurpy[HashRef[Dict[type => Enum[qw(null object array boolean string number integer)], sub => CodeRef]]],
106             ],
107             init_arg => 'format_validations',
108             handles_via => 'Hash',
109             handles => {
110             _get_format_validation => 'get',
111             add_format_validation => 'set',
112             },
113             lazy => 1,
114             default => sub { {} },
115             );
116              
117             before add_format_validation => sub ($self, @kvs) { $format_type->({ @$_ }) foreach pairs @kvs };
118              
119             around BUILDARGS => sub ($orig, $class, @args) {
120             my $args = $class->$orig(@args);
121             croak 'output_format: strict_basic can only be used with specification_version: draft2019-09'
122             if ($args->{output_format}//'') eq 'strict_basic'
123             and ($args->{specification_version}//'') ne 'draft2019-09';
124              
125             return $args;
126             };
127              
128             sub add_schema {
129 13700 50   13700 1 377271 croak 'insufficient arguments' if @_ < 2;
130 13700         22800 my $self = shift;
131              
132             # TODO: resolve $uri against $self->base_uri
133 13700 50       49947 my $uri = !is_ref($_[0]) ? Mojo::URL->new(shift)
    100          
134             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
135              
136 13700 100       2244290 croak 'cannot add a schema with a uri with a fragment' if defined $uri->fragment;
137              
138 13699 100       85179 if (not @_) {
139 2         8 my $schema_info = $self->_fetch_from_uri($uri);
140 2 100 66     23 return if not $schema_info or not defined wantarray;
141 1         13 return $schema_info->{document};
142             }
143              
144             # document BUILD will trigger $self->traverse($schema)
145 13697 50       40235 my $document = $_[0]->$_isa('JSON::Schema::Modern::Document') ? shift
    100          
146             : JSON::Schema::Modern::Document->new(
147             schema => shift,
148             $uri ? (canonical_uri => $uri) : (),
149             evaluator => $self, # used mainly for traversal during document construction
150             );
151              
152 13697 100       741242 if ($document->has_errors) {
153 117         8363 my $result = JSON::Schema::Modern::Result->new(
154             output_format => $self->output_format,
155             valid => 0,
156             errors => [ $document->errors ],
157             exception => 1,
158             );
159 117         10529 die $result;
160             }
161              
162 13580 100       706473 if (not grep refaddr($_->{document}) == refaddr($document), $self->_canonical_resources) {
163 13575   66     1686018 my $schema_content = $document->_serialized_schema
164             // $document->_serialized_schema($self->_json_decoder->encode($document->schema));
165              
166 13575 100       1121236 if (my $existing_doc = first {
167 420259   66 420259   7833589 my $existing_content = $_->_serialized_schema
168             // $_->_serialized_schema($self->_json_decoder->encode($_->schema));
169 420259         2648847 $existing_content eq $schema_content
170             } uniqint map $_->{document}, $self->_canonical_resources) {
171             # we already have this schema content in another document object.
172 9503         76477 $document = $existing_doc;
173             }
174             else {
175 4072         78873 $self->_add_resources(map +($_->[0] => +{ $_->[1]->%*, document => $document }),
176             $document->resource_pairs);
177             }
178             }
179              
180 13574 100       763658 if ("$uri") {
181 382         77809 my $resource = $document->_get_resource($document->canonical_uri);
182             $self->_add_resources($uri => {
183             path => '',
184             canonical_uri => $document->canonical_uri,
185             specification_version => $resource->{specification_version},
186             vocabularies => $resource->{vocabularies}, # reference, not copy
187             document => $document,
188             configs => $resource->{configs},
189 382         112101 });
190             }
191              
192 13574         1778233 return $document;
193             }
194              
195 3     3 1 12981 sub evaluate_json_string ($self, $json_data, $schema, $config_override = {}) {
  3         9  
  3         7  
  3         6  
  3         11  
  3         6  
196 3 50       14 croak 'evaluate_json_string called in void context' if not defined wantarray;
197              
198 3         6 my $data;
199             try {
200             $data = $self->_json_decoder->decode($json_data)
201             }
202 3         17 catch ($e) {
203             return JSON::Schema::Modern::Result->new(
204             output_format => $self->output_format,
205             valid => 0,
206             exception => 1,
207             errors => [
208             JSON::Schema::Modern::Error->new(
209             keyword => undef,
210             instance_location => '',
211             keyword_location => '',
212             error => $e,
213             )
214             ],
215             );
216             }
217              
218 1         19 return $self->evaluate($data, $schema, $config_override);
219             }
220              
221             # this is called whenever we need to walk a document for something.
222             # for now it is just called when a ::Document object is created, to verify the integrity of the
223             # schema structure, to identify the metaschema (via the $schema keyword), and to extract all
224             # embedded resources via $id and $anchor keywords within.
225             # Returns the internal $state object accumulated during the traversal.
226 13797     13797 1 689200 sub traverse ($self, $schema_reference, $config_override = {}) {
  13797         23508  
  13797         21829  
  13797         22575  
  13797         20547  
227             # Note: the starting position is not guaranteed to be at the root of the $document.
228 13797   100     45921 my $initial_uri = Mojo::URL->new($config_override->{initial_schema_uri} // '');
229 13797   100     4117280 my $initial_path = $config_override->{traversed_schema_path} // '';
230 13797   100     57757 my $spec_version = $self->specification_version//SPECIFICATION_VERSION_DEFAULT;
231              
232             my $state = {
233             depth => 0,
234             data_path => '', # this never changes since we don't have an instance yet
235             initial_schema_uri => $initial_uri, # the canonical URI as of the start of this method, or last $id
236             traversed_schema_path => $initial_path, # the accumulated traversal path as of the start, or last $id
237             schema_path => '', # the rest of the path, since the start of this method, or last $id
238             effective_base_uri => Mojo::URL->new(''),
239             errors => [],
240             identifiers => [],
241             configs => {},
242             callbacks => $config_override->{callbacks} // {},
243 13797   100     44664 evaluator => $self,
244             traverse => 1,
245             };
246              
247             try {
248             my $for_canonical_uri = Mojo::URL->new(
249             (is_plain_hashref($schema_reference) && exists $schema_reference->{'$id'}
250             ? Mojo::URL->new($schema_reference->{'$id'}) : undef)
251             // $state->{initial_schema_uri});
252             $for_canonical_uri->fragment(undef) if not length $for_canonical_uri->fragment;
253              
254             # a subsequent "$schema" keyword can still change these values
255             $state->@{qw(spec_version vocabularies)} = $self->_get_metaschema_info(
256             $config_override->{metaschema_uri} // $self->METASCHEMA_URIS->{$spec_version},
257             $for_canonical_uri,
258             );
259             }
260 13797         2346836 catch ($e) {
261             if ($e->$_isa('JSON::Schema::Modern::Result')) {
262             push $state->{errors}->@*, $e->errors;
263             }
264             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
265             push $state->{errors}->@*, $e;
266             }
267             else {
268             ()= E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
269             }
270              
271             return $state;
272             }
273              
274             try {
275             $self->_traverse_subschema($schema_reference, $state);
276             }
277 13794         36956 catch ($e) {
278             if ($e->$_isa('JSON::Schema::Modern::Error')) {
279             # note: we should never be here, since traversal subs are no longer be fatal
280             push $state->{errors}->@*, $e;
281             }
282             else {
283             E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
284             }
285             }
286              
287 13794         28528 delete $state->{traverse};
288 13794         46889 return $state;
289             }
290              
291             # the actual runtime evaluation of the schema against input data.
292 13325     13325 1 21170384 sub evaluate ($self, $data, $schema_reference, $config_override = {}) {
  13325         24569  
  13325         22130  
  13325         20443  
  13325         26088  
  13325         20589  
293 13325 50       37315 croak 'evaluate called in void context' if not defined wantarray;
294              
295 13325   100     54419 my $initial_path = $config_override->{traversed_schema_path} // '';
296 13325   100     72863 my $effective_base_uri = Mojo::URL->new($config_override->{effective_base_uri}//'');
297              
298             my $state = {
299 13325   100     2522211 data_path => $config_override->{data_path} // '',
300             traversed_schema_path => $initial_path, # the accumulated path as of the start of evaluation, or last $id or $ref
301             initial_schema_uri => Mojo::URL->new, # the canonical URI as of the start of evaluation, or last $id or $ref
302             schema_path => '', # the rest of the path, since the start of evaluation, or last $id or $ref
303             effective_base_uri => $effective_base_uri, # resolve locations against this for errors and annotations
304             errors => [],
305             };
306              
307             exists $config_override->{$_} and die $_.' not supported as a config override'
308 13325   50     190078 foreach qw(output_format specification_version);
309              
310 13325         23343 my $valid;
311             try {
312             my $schema_info;
313              
314             if (not is_ref($schema_reference) or $schema_reference->$_isa('Mojo::URL')) {
315             $schema_info = $self->_fetch_from_uri($schema_reference);
316             $state->{initial_schema_uri} = Mojo::URL->new($config_override->{initial_schema_uri} // '');
317             }
318             else {
319             # traverse is called via add_schema -> ::Document->new -> ::Document->BUILD
320             my $document = $self->add_schema('', $schema_reference);
321             my $base_resource = $document->_get_resource($document->canonical_uri)
322             || croak "couldn't get resource: document parse error";
323              
324             $schema_info = {
325             schema => $document->schema,
326             document => $document,
327             document_path => '',
328             $base_resource->%{qw(canonical_uri specification_version vocabularies configs)},
329             };
330             }
331              
332             abort($state, 'EXCEPTION: unable to find resource %s', $schema_reference)
333             if not $schema_info;
334              
335             $state = +{
336             %$state,
337             depth => 0,
338             initial_schema_uri => $schema_info->{canonical_uri}, # the canonical URI as of the start of evaluation, or last $id or $ref
339             document => $schema_info->{document}, # the ::Document object containing this schema
340             document_path => $schema_info->{document_path}, # the path within the document of this schema, as of the start of evaluation, or last $id or $ref
341             dynamic_scope => [ $schema_info->{canonical_uri} ],
342             annotations => [],
343             seen => {},
344             spec_version => $schema_info->{specification_version},
345             vocabularies => $schema_info->{vocabularies},
346             callbacks => $config_override->{callbacks} // {},
347             evaluator => $self,
348             $schema_info->{configs}->%*,
349             (map {
350             my $val = $config_override->{$_} // $self->$_;
351             defined $val ? ( $_ => $val ) : ()
352             } qw(validate_formats validate_content_schemas short_circuit collect_annotations scalarref_booleans stringy_numbers strict)),
353             };
354              
355             if ($state->{validate_formats}) {
356             $state->{vocabularies} = [
357             map s/^JSON::Schema::Modern::Vocabulary::Format\KAnnotation$/Assertion/r, $state->{vocabularies}->@*
358             ];
359             require JSON::Schema::Modern::Vocabulary::FormatAssertion;
360             }
361              
362             # we're going to set collect_annotations during evaluation when we see an unevaluated* keyword,
363             # but after we pass to a new data scope we'll clear it again.. unless we've got the config set
364             # globally for the entire evaluation, so we store that value in a high bit.
365             $state->{collect_annotations} = ($state->{collect_annotations}//0) << 8;
366              
367             $valid = $self->_eval_subschema($data, $schema_info->{schema}, $state);
368             warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
369             }
370 13325         32237 catch ($e) {
371             if ($e->$_isa('JSON::Schema::Modern::Result')) {
372             return $e;
373             }
374             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
375             push $state->{errors}->@*, $e;
376             }
377             else {
378             $valid = E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
379             }
380             }
381              
382 13208 50 50     64994 die 'evaluate validity inconsistent with error count' if $valid xor !$state->{errors}->@*;
383              
384             return JSON::Schema::Modern::Result->new(
385             output_format => $self->output_format,
386             valid => $valid,
387             $valid
388             # strip annotations from result if user didn't explicitly ask for them
389             ? ($config_override->{collect_annotations} // $self->collect_annotations
390             ? (annotations => $state->{annotations}) : ())
391 13208 100 100     365614 : (errors => $state->{errors}),
    100          
392             );
393             }
394              
395 3     3 1 23753 sub validate_schema ($self, $schema, $config_override = {}) {
  3         7  
  3         6  
  3         8  
  3         4  
396 3 50       14 croak 'validate_schema called in void context' if not defined wantarray;
397              
398             my $metaschema_uri = is_plain_hashref($schema) && $schema->{'$schema'} ? $schema->{'$schema'}
399 3 100 66     35 : $self->METASCHEMA_URIS->{$self->specification_version // $self->SPECIFICATION_VERSION_DEFAULT};
      33        
400              
401 3         15 return $self->evaluate($schema, $metaschema_uri, $config_override);
402             }
403              
404 10     10 1 76870 sub get ($self, $uri) {
  10         22  
  10         19  
  10         19  
405 10         40 my $schema_info = $self->_fetch_from_uri($uri);
406 10 100       814 return if not $schema_info;
407 8 100       966 my $subschema = is_ref($schema_info->{schema}) ? dclone($schema_info->{schema}) : $schema_info->{schema};
408 8 100       199 return wantarray ? ($subschema, $schema_info->{canonical_uri}) : $subschema;
409             }
410              
411             # defined lower down:
412             # sub add_vocabulary { ... }
413             # sub add_encoding { ... }
414             # sub add_media_type { ... }
415              
416             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
417              
418             # current spec version => { keyword => undef, or arrayref of alternatives }
419             my %removed_keywords = (
420             'draft7' => {
421             id => [ '$id' ],
422             },
423             'draft2019-09' => {
424             id => [ '$id' ],
425             definitions => [ '$defs' ],
426             dependencies => [ qw(dependentSchemas dependentRequired) ],
427             },
428             'draft2020-12' => {
429             id => [ '$id' ],
430             definitions => [ '$defs' ],
431             dependencies => [ qw(dependentSchemas dependentRequired) ],
432             '$recursiveAnchor' => [ '$dynamicAnchor' ],
433             '$recursiveRef' => [ '$dynamicRef' ],
434             additionalItems => [ 'items' ],
435             },
436             );
437              
438             # {
439             # $spec_version => {
440             # $vocabulary_class => {
441             # traverse => [ [ $keyword => $subref ], [ ... ] ],
442             # evaluate => [ [ $keyword => $subref ], [ ... ] ],
443             # }
444             # }
445             # }
446             # If we could serialize coderefs, this could be an object attribute;
447             # otherwise, we might as well persist this for the lifetime of the process.
448             our $vocabulary_cache = {};
449              
450 32619     32619   56202 sub _traverse_subschema ($self, $schema, $state) {
  32619         49852  
  32619         47670  
  32619         45799  
  32619         45470  
451 32619         58890 delete $state->{keyword};
452              
453             return E($state, 'EXCEPTION: maximum traversal depth exceeded')
454 32619 50       110435 if $state->{depth}++ > $self->max_traversal_depth;
455              
456 32619         87822 my $schema_type = get_type($schema);
457 32619 100       170007 return 1 if $schema_type eq 'boolean';
458              
459 25797 100       55089 return E($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
460              
461 25787 100       67056 return 1 if not keys %$schema;
462              
463 25300         38859 my $valid = 1;
464 25300         124153 my %unknown_keywords = map +($_ => undef), keys %$schema;
465             # we must check the array length on every iteration because some keywords can change it!
466 25300         88156 for (my $idx = 0; $idx <= $state->{vocabularies}->$#*; ++$idx) {
467 161697         269221 my $vocabulary = $state->{vocabularies}[$idx];
468              
469             # [ [ $keyword => $subref ], [ ... ] ]
470             my $keyword_list = $vocabulary_cache->{$state->{spec_version}}{$vocabulary}{traverse} //= [
471             map [ $_ => $vocabulary->can('_traverse_keyword_'.($_ =~ s/^\$//r)) ],
472             $vocabulary->keywords($state->{spec_version})
473 161697   100     446150 ];
474              
475 161697         267349 foreach my $keyword_tuple ($keyword_list->@*) {
476 1369844         2123927 my ($keyword, $sub) = $keyword_tuple->@*;
477 1369844 100       2742418 next if not exists $schema->{$keyword};
478              
479             # keywords adjacent to $ref are not evaluated before draft2019-09
480 45540 100 100     180150 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{spec_version} eq 'draft7';
      100        
481              
482 45514         90562 delete $unknown_keywords{$keyword};
483 45514         81761 $state->{keyword} = $keyword;
484              
485 45514 100       154465 if (not $sub->($vocabulary, $schema, $state)) {
486 179 50       635 die 'traverse result is false but there are no errors (keyword: '.$keyword.')' if not $state->{errors}->@*;
487 179         325 $valid = 0;
488 179         496 next;
489             }
490              
491 45335 100       142602 if (my $callback = $state->{callbacks}{$keyword}) {
492 4 50       18 if (not $callback->($schema, $state)) {
493 0 0       0 die 'callback result is false but there are no errors (keyword: '.$keyword.')' if not $state->{errors}->@*;
494 0         0 $valid = 0;
495 0         0 next;
496             }
497             }
498             }
499             }
500              
501 25300         49264 delete $state->{keyword};
502              
503 25300 100 100     79671 if ($self->strict and keys %unknown_keywords) {
504 1 50       16 ()= E($state, 'unknown keyword%s found: %s', keys %unknown_keywords > 1 ? 's' : '',
505             join(', ', sort keys %unknown_keywords));
506             }
507              
508             # check for previously-supported but now removed keywords
509 25300         120481 foreach my $keyword (sort keys $removed_keywords{$state->{spec_version}}->%*) {
510 94809 100       199954 next if not exists $schema->{$keyword};
511 238         1091 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
512             .canonical_uri($state).'")';
513 238 50       30890 if (my $alternates = $removed_keywords{$state->{spec_version}}->{$keyword}) {
514 238         1256 my @list = map '"'.$_.'"', @$alternates;
515 238 50       721 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
516 238 100       735 splice(@list, -1, 0, 'or') if @list > 1;
517 238         831 $message .= ': this should be rewritten as '.join(' ', @list);
518             }
519 238         53134 carp $message;
520             }
521              
522 25300         131491 return $valid;
523             }
524              
525 27852     27852   47867 sub _eval_subschema ($self, $data, $schema, $state) {
  27852         43919  
  27852         44906  
  27852         39877  
  27852         38613  
  27852         37723  
526 27852 50       64006 croak '_eval_subschema called in void context' if not defined wantarray;
527              
528             # callers created a new $state for us, so we do not propagate upwards changes to depth, traversed
529             # paths; but annotations, errors are arrayrefs so their contents will be shared
530 27852   100     89519 $state->{dynamic_scope} = [ ($state->{dynamic_scope}//[])->@* ];
531 27852         239227 delete $state->@{'keyword', grep /^_/, keys %$state};
532              
533             abort($state, 'EXCEPTION: maximum evaluation depth exceeded')
534 27852 100       129172 if $state->{depth}++ > $self->max_traversal_depth;
535              
536 27849         90328 my $schema_type = get_type($schema);
537 27849 100 66     77532 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
538              
539             # this should never happen, due to checks in traverse
540 27081 50       61731 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
541              
542 27081 100       66472 return 1 if not keys %$schema;
543              
544             # find all schema locations in effect at this data path + canonical_uri combination
545             # if any of them are absolute prefix of this schema location, we are in a loop.
546 26821         65903 my $canonical_uri = canonical_uri($state);
547 26821         69229 my $schema_location = $state->{traversed_schema_path}.$state->{schema_path};
548             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
549             if grep substr($schema_location, 0, length) eq $_,
550 26821 100       121223 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
551 26820         4162057 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
552              
553 26820         3713274 my $valid = 1;
554 26820         151047 my %unknown_keywords = map +($_ => undef), keys %$schema;
555              
556             # set aside annotations collected so far; they are not used in the current scope's evaluation
557 26820         64239 my $parent_annotations = $state->{annotations};
558 26820         55221 $state->{annotations} = [];
559              
560             # in order to collect annotations from applicator keywords only when needed, we twiddle the low
561             # bit if we see a local unevaluated* keyword, and clear it again as we move on to a new data path.
562 26820   100     124946 $state->{collect_annotations} |= 0+(exists $schema->{unevaluatedItems} || exists $schema->{unevaluatedProperties});
563              
564             # in order to collect annotations for unevaluated* keywords, we sometimes need to ignore the
565             # suggestion to short_circuit evaluation at this scope (but lower scopes are still fine)
566             $state->{short_circuit} = ($state->{short_circuit} || delete($state->{short_circuit_suggested}))
567 26820   100     142765 && !exists($schema->{unevaluatedItems}) && !exists($schema->{unevaluatedProperties});
568              
569             ALL_KEYWORDS:
570 26820         73855 foreach my $vocabulary ($state->{vocabularies}->@*) {
571             # [ [ $keyword => $subref|undef ], [ ... ] ]
572             my $keyword_list = $vocabulary_cache->{$state->{spec_version}}{$vocabulary}{evaluate} //= [
573             map [ $_ => $vocabulary->can('_eval_keyword_'.($_ =~ s/^\$//r)) ],
574             $vocabulary->keywords($state->{spec_version})
575 155920   100     446674 ];
576              
577 155920         256376 foreach my $keyword_tuple ($keyword_list->@*) {
578 1332620         2077027 my ($keyword, $sub) = $keyword_tuple->@*;
579 1332620 100       2541510 next if not exists $schema->{$keyword};
580              
581             # keywords adjacent to $ref are not evaluated before draft2019-09
582 55150 100 100     208900 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{spec_version} eq 'draft7';
      100        
583              
584 55139         105193 delete $unknown_keywords{$keyword};
585 55139         103359 $state->{keyword} = $keyword;
586              
587 55139 100       109532 if ($sub) {
588 43117         76225 my $error_count = $state->{errors}->@*;
589              
590 43117 100       148672 if (not $sub->($vocabulary, $data, $schema, $state)) {
591             warn 'evaluation result is false but there are no errors (keyword: '.$keyword.')'
592 10578 50       36523 if $error_count == $state->{errors}->@*;
593 10578         19009 $valid = 0;
594              
595 10578 100       33124 last ALL_KEYWORDS if $state->{short_circuit};
596 6456         18657 next;
597             }
598             }
599              
600 44494 100       206979 if (my $callback = $state->{callbacks}{$keyword}) {
601 19         62 my $error_count = $state->{errors}->@*;
602              
603 19 100       78 if (not $callback->($data, $schema, $state)) {
604             warn 'callback result is false but there are no errors (keyword: '.$keyword.')'
605 2 50       9 if $error_count == $state->{errors}->@*;
606 2         5 $valid = 0;
607              
608 2 100       10 last ALL_KEYWORDS if $state->{short_circuit};
609 1         4 next;
610             }
611             }
612             }
613             }
614              
615 26753         52688 delete $state->{keyword};
616              
617 26753 100 66     65281 if ($state->{strict} and keys %unknown_keywords) {
618 2 50       19 abort($state, 'unknown keyword%s found: %s', keys %unknown_keywords > 1 ? 's' : '',
619             join(', ', sort keys %unknown_keywords));
620             }
621              
622 26751 100 100     101403 if ($valid and $state->{collect_annotations} and $state->{spec_version} !~ qr/^draft(7|2019-09)$/) {
      100        
623             annotate_self(+{ %$state, keyword => $_, _unknown => 1 }, $schema)
624 824         2807 foreach sort keys %unknown_keywords;
625             }
626              
627             # only keep new annotations if schema is valid
628 26751 100       62467 push $parent_annotations->@*, $state->{annotations}->@* if $valid;
629              
630 26751         194656 return $valid;
631             }
632              
633             has _resource_index => (
634             is => 'bare',
635             isa => HashRef[my $resource_type = Dict[
636             canonical_uri => InstanceOf['Mojo::URL'],
637             path => Str,
638             specification_version => my $spec_version_type = Enum(SPECIFICATION_VERSIONS_SUPPORTED),
639             document => InstanceOf['JSON::Schema::Modern::Document'],
640             # the vocabularies used when evaluating instance data against schema
641             vocabularies => ArrayRef[my $vocabulary_class_type = ClassName->where(q{$_->DOES('JSON::Schema::Modern::Vocabulary')})],
642             configs => HashRef,
643             Slurpy[HashRef[Undef]], # no other fields allowed
644             ]],
645             handles_via => 'Hash',
646             handles => {
647             _add_resources => 'set',
648             _get_resource => 'get',
649             _remove_resource => 'delete',
650             _resource_index => 'elements',
651             _resource_keys => 'keys',
652             _add_resources_unsafe => 'set',
653             _canonical_resources => 'values',
654             _resource_exists => 'exists',
655             },
656             lazy => 1,
657             default => sub { {} },
658             );
659              
660             around _add_resources => sub {
661             my ($orig, $self) = (shift, shift);
662              
663             my @resources;
664             foreach my $pair (sort { $a->[0] cmp $b->[0] } pairs @_) {
665             my ($key, $value) = @$pair;
666              
667             $resource_type->($value); # check type of hash value against Dict
668              
669             if (my $existing = $self->_get_resource($key)) {
670             # we allow overwriting canonical_uri = '' to allow for ad hoc evaluation of schemas that
671             # lack all identifiers altogether, but preserve other resources from the original document
672             if ($key ne '') {
673             next if $existing->{path} eq $value->{path}
674             and $existing->{canonical_uri} eq $value->{canonical_uri}
675             and $existing->{specification_version} eq $value->{specification_version}
676             and refaddr($existing->{document}) == refaddr($value->{document});
677             croak 'uri "'.$key.'" conflicts with an existing schema resource';
678             }
679             }
680             elsif ($self->CACHED_METASCHEMAS->{$key}) {
681             croak 'uri "'.$key.'" conflicts with an existing meta-schema resource';
682             }
683              
684             my $fragment = $value->{canonical_uri}->fragment;
685             croak sprintf('canonical_uri cannot contain an empty fragment (%s)', $value->{canonical_uri})
686             if defined $fragment and $fragment eq '';
687              
688             croak sprintf('canonical_uri cannot contain a plain-name fragment (%s)', $value->{canonical_uri})
689             if ($fragment // '') =~ m{^[^/]};
690              
691             $self->$orig($key, $value);
692             }
693             };
694              
695             # $vocabulary uri (not its $id!) => [ spec_version, class ]
696             has _vocabulary_classes => (
697             is => 'bare',
698             isa => HashRef[
699             Tuple[
700             $spec_version_type,
701             $vocabulary_class_type,
702             ]
703             ],
704             handles_via => 'Hash',
705             handles => {
706             _get_vocabulary_class => 'get',
707             _set_vocabulary_class => 'set',
708             _get_vocabulary_values => 'values',
709             },
710             lazy => 1,
711             default => sub {
712             +{
713             map { my $class = $_; pairmap { $a => [ $b, $class ] } $class->vocabulary }
714             map use_module('JSON::Schema::Modern::Vocabulary::'.$_),
715             qw(Core Applicator Validation FormatAssertion FormatAnnotation Content MetaData Unevaluated)
716             }
717             },
718             );
719              
720 8     8 1 25440 sub add_vocabulary ($self, $classname) {
  8         19  
  8         18  
  8         16  
721 8 50       208 return if grep $_->[1] eq $classname, $self->_get_vocabulary_values;
722              
723 8         1128 $vocabulary_class_type->(use_module($classname));
724              
725             # uri => version, uri => version
726 5         9162 foreach my $pair (pairs $classname->vocabulary) {
727 5         78 my ($uri_string, $spec_version) = @$pair;
728 5         20 Str->where(q{my $uri = Mojo::URL->new($_); $uri->is_abs && !defined $uri->fragment})->($uri_string);
729 4         5880 $spec_version_type->($spec_version);
730 2         343 $self->_set_vocabulary_class($uri_string => [ $spec_version, $classname ])
731             }
732             }
733              
734             # $schema uri => [ spec_version, [ vocab classes ] ].
735             has _metaschema_vocabulary_classes => (
736             is => 'bare',
737             isa => HashRef[
738             Tuple[
739             $spec_version_type,
740             ArrayRef[$vocabulary_class_type],
741             ]
742             ],
743             handles_via => 'Hash',
744             handles => {
745             _get_metaschema_vocabulary_classes => 'get',
746             _set_metaschema_vocabulary_classes => 'set',
747             __all_metaschema_vocabulary_classes => 'values',
748             },
749             lazy => 1,
750             default => sub {
751             my @modules = map use_module('JSON::Schema::Modern::Vocabulary::'.$_),
752             qw(Core Applicator Validation FormatAnnotation Content MetaData Unevaluated);
753             +{
754             'https://json-schema.org/draft/2020-12/schema' => [ 'draft2020-12', [ @modules ] ],
755             do { pop @modules; () },
756             'https://json-schema.org/draft/2019-09/schema' => [ 'draft2019-09', \@modules ],
757             'http://json-schema.org/draft-07/schema#' => [ 'draft7', \@modules ],
758             },
759             },
760             );
761              
762             # retrieves metaschema info either from cache or by parsing the schema for vocabularies
763             # throws a JSON::Schema::Modern::Result on error
764 13797     13797   24713 sub _get_metaschema_info ($self, $metaschema_uri, $for_canonical_uri) {
  13797         23498  
  13797         23756  
  13797         19950  
  13797         19067  
765             # check the cache
766 13797         315034 my $metaschema_info = $self->_get_metaschema_vocabulary_classes($metaschema_uri);
767 13797 100       1592976 return @$metaschema_info if $metaschema_info;
768              
769             # otherwise, fetch the metaschema and parse its $vocabulary keyword.
770             # we do this by traversing a baby schema with just the $schema keyword.
771 5         35 my $state = $self->traverse({ '$schema' => $metaschema_uri.'' });
772             die JSON::Schema::Modern::Result->new(
773             output_format => $self->output_format,
774             valid => JSON::PP::false,
775             errors => [
776             map {
777 9         429 my $e = $_;
778             # absolute location is undef iff the location = '/$schema'
779 9   66     50 my $absolute_location = $e->absolute_keyword_location // $for_canonical_uri;
780 9 100 100     72 JSON::Schema::Modern::Error->new(
    100          
781             keyword => $e->keyword eq '$schema' ? '' : $e->keyword,
782             instance_location => $e->instance_location,
783             keyword_location => ($for_canonical_uri->fragment//'').($e->keyword_location =~ s{^/\$schema\b}{}r),
784             length $absolute_location ? ( absolute_keyword_location => $absolute_location ) : (),
785             error => $e->error,
786             )
787             }
788             $state->{errors}->@* ],
789             exception => 1,
790 5 100       40 ) if $state->{errors}->@*;
791 2         24 return ($state->{spec_version}, $state->{vocabularies});
792             }
793              
794             # used for determining a default '$schema' keyword where there is none
795 34         5312 use constant METASCHEMA_URIS => {
796             'draft2020-12' => 'https://json-schema.org/draft/2020-12/schema',
797             'draft2019-09' => 'https://json-schema.org/draft/2019-09/schema',
798             'draft7' => 'http://json-schema.org/draft-07/schema#',
799 34     34   372 };
  34         93  
800              
801 34         79966 use constant CACHED_METASCHEMAS => {
802             'https://json-schema.org/draft/2020-12/meta/applicator' => 'draft2020-12/meta/applicator.json',
803             'https://json-schema.org/draft/2020-12/meta/content' => 'draft2020-12/meta/content.json',
804             'https://json-schema.org/draft/2020-12/meta/core' => 'draft2020-12/meta/core.json',
805             'https://json-schema.org/draft/2020-12/meta/format-annotation' => 'draft2020-12/meta/format-annotation.json',
806             'https://json-schema.org/draft/2020-12/meta/format-assertion' => 'draft2020-12/meta/format-assertion.json',
807             'https://json-schema.org/draft/2020-12/meta/meta-data' => 'draft2020-12/meta/meta-data.json',
808             'https://json-schema.org/draft/2020-12/meta/unevaluated' => 'draft2020-12/meta/unevaluated.json',
809             'https://json-schema.org/draft/2020-12/meta/validation' => 'draft2020-12/meta/validation.json',
810             'https://json-schema.org/draft/2020-12/output/schema' => 'draft2020-12/output/schema.json',
811             'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12/schema.json',
812              
813             'https://json-schema.org/draft/2019-09/meta/applicator' => 'draft2019-09/meta/applicator.json',
814             'https://json-schema.org/draft/2019-09/meta/content' => 'draft2019-09/meta/content.json',
815             'https://json-schema.org/draft/2019-09/meta/core' => 'draft2019-09/meta/core.json',
816             'https://json-schema.org/draft/2019-09/meta/format' => 'draft2019-09/meta/format.json',
817             'https://json-schema.org/draft/2019-09/meta/meta-data' => 'draft2019-09/meta/meta-data.json',
818             'https://json-schema.org/draft/2019-09/meta/validation' => 'draft2019-09/meta/validation.json',
819             'https://json-schema.org/draft/2019-09/output/schema' => 'draft2019-09/output/schema.json',
820             'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09/schema.json',
821              
822             # trailing # is omitted because we always cache documents by its canonical (fragmentless) URI
823             'http://json-schema.org/draft-07/schema' => 'draft7/schema.json',
824 34     34   344 };
  34         93  
825              
826             # returns the same as _get_resource
827 3761     3761   7610 sub _get_or_load_resource ($self, $uri) {
  3761         6071  
  3761         5455  
  3761         5458  
828 3761         87849 my $resource = $self->_get_resource($uri);
829 3761 100       1216847 return $resource if $resource;
830              
831 78 100       449 if (my $local_filename = $self->CACHED_METASCHEMAS->{$uri}) {
832 72         19190 my $file = path(dist_dir('JSON-Schema-Modern'), $local_filename);
833 72         14498 my $schema = $self->_json_decoder->decode($file->slurp_raw);
834 72         23501 my $document = JSON::Schema::Modern::Document->new(schema => $schema, evaluator => $self);
835              
836             # this should be caught by the try/catch in evaluate()
837 72 50       43154 die JSON::Schema::Modern::Result->new(
838             output_format => $self->output_format,
839             valid => 0,
840             errors => [ $document->errors ],
841             exception => 1,
842             ) if $document->has_errors;
843              
844             # we have already performed the appropriate collision checks, so we bypass them here
845 72         4228 $self->_add_resources_unsafe(
846             map +($_->[0] => +{ $_->[1]->%*, document => $document }),
847             $document->resource_pairs
848             );
849              
850 72         15010 return $self->_get_resource($uri);
851             }
852              
853             # TODO:
854             # - load from network or disk
855              
856 6         1286 return;
857             };
858              
859             # returns information necessary to use a schema found at a particular URI:
860             # - a schema (which may not be at a document root)
861             # - the canonical uri for that schema,
862             # - the JSON::Schema::Modern::Document object that holds that schema
863             # - the path relative to the document root for this schema
864             # - the specification version that applies to this schema
865             # - the vocabularies to use when considering schema keywords
866             # - the config overrides to set when considering schema keywords
867             # creates a Document and adds it to the resource index, if not already present.
868 4582     4582   7607 sub _fetch_from_uri ($self, $uri) {
  4582         7108  
  4582         7346  
  4582         6454  
869 4582 100       13311 $uri = Mojo::URL->new($uri) if not is_ref($uri);
870 4582         22994 my $fragment = $uri->fragment;
871              
872 4582 100 100     38176 if (not length($fragment) or $fragment =~ m{^/}) {
873 3760         11057 my $base = $uri->clone->fragment(undef);
874 3760 100       464974 if (my $resource = $self->_get_or_load_resource($base)) {
875 3754   100     52972 my $subschema = $resource->{document}->get(my $document_path = $resource->{path}.($fragment//''));
876 3754 100       73136 return if not defined $subschema;
877 3748         7088 my $document = $resource->{document};
878             my $closest_resource = first { !length($_->[1]{path}) # document root
879 3956 100 100 3956   389689 || length($document_path)
880             && $document_path =~ m{^\Q$_->[1]{path}\E(?:/|\z)} } # path is above present location
881 1385         85974 sort { length($b->[1]{path}) <=> length($a->[1]{path}) } # sort by length, descending
882 3748         92110 grep { not length Mojo::URL->new($_->[0])->fragment } # omit anchors
  6173         500975  
883             $document->resource_pairs;
884              
885             my $canonical_uri = $closest_resource->[1]{canonical_uri}->clone
886 3748         25056 ->fragment(substr($document_path, length($closest_resource->[1]{path})));
887 3748 100       329358 $canonical_uri->fragment(undef) if not length($canonical_uri->fragment);
888             return {
889             schema => $subschema,
890             canonical_uri => $canonical_uri,
891             document => $document,
892             document_path => $document_path,
893 3748         74331 $resource->%{qw(specification_version vocabularies configs)}, # reference, not copy
894             };
895             }
896             }
897             else { # we are following a URI with a plain-name fragment
898 822 100       18679 if (my $resource = $self->_get_resource($uri)) {
899 720         240762 my $subschema = $resource->{document}->get($resource->{path});
900 720 50       14182 return if not defined $subschema;
901             return {
902             schema => $subschema,
903             canonical_uri => $resource->{canonical_uri}->clone, # this is *not* the anchor-containing URI
904             document => $resource->{document},
905             document_path => $resource->{path},
906 720         2765 $resource->%{qw(specification_version vocabularies configs)}, # reference, not copy
907             };
908             }
909             }
910             }
911              
912             # used for internal encoding as well (when caching serialized schemas)
913             has _json_decoder => (
914             is => 'ro',
915             isa => HasMethods[qw(encode decode)],
916             lazy => 1,
917             default => sub { JSON::MaybeXS->new(allow_nonref => 1, canonical => 1, utf8 => 1, allow_bignum => 1, convert_blessed => 1) },
918             );
919              
920             # since media types are case-insensitive, all type names must be foldcased on insertion.
921             has _media_type => (
922             is => 'bare',
923             isa => my $media_type_type = Map[Str->where(q{$_ eq CORE::fc($_)}), CodeRef],
924             handles_via => 'Hash',
925             handles => {
926             get_media_type => 'get',
927             add_media_type => 'set',
928             _media_types => 'keys',
929             },
930             lazy => 1,
931             default => sub ($self) {
932             my $_json_media_type = sub ($content_ref) {
933             # utf-8 decoding is always done, as per the JSON spec.
934             # other charsets are not supported: see RFC8259 §11
935             \ JSON::MaybeXS->new(allow_nonref => 1, utf8 => 1)->decode($content_ref->$*);
936             };
937             +{
938             (map +($_ => $_json_media_type),
939             qw(application/json application/schema+json application/schema-instance+json)),
940             (map +($_ => sub ($content_ref) { $content_ref }),
941             qw(text/* application/octet-stream)),
942             'application/x-www-form-urlencoded' => sub ($content_ref) {
943             \ Mojo::Parameters->new->charset('UTF-8')->parse($content_ref->$*)->to_hash;
944             },
945             'application/x-ndjson' => sub ($content_ref) {
946             my $decoder = JSON::MaybeXS->new(allow_nonref => 1, utf8 => 1);
947             my $line = 0; # line numbers start at 1
948             \[ map {
949             do {
950             try { ++$line; $decoder->decode($_) }
951             catch ($e) { die 'parse error at line '.$line.': '.$e }
952             }
953             }
954             split(/\r?\n/, $content_ref->$*)
955             ];
956             },
957             };
958             },
959             );
960              
961             # get_media_type('TExT/bloop') will fall through to matching an entry for 'text/*' or '*/*'
962             around get_media_type => sub ($orig, $self, $type) {
963             my $mt = $self->$orig(fc $type);
964             return $mt if $mt;
965              
966             return $self->$orig((first { m{([^/]+)/\*$} && fc($type) =~ m{^\Q$1\E/[^/]+$} } $self->_media_types)
967             // '*/*');
968             };
969              
970             before add_media_type => sub ($self, $type, $sub) { $media_type_type->({ $type => $sub }) };
971              
972             has _encoding => (
973             is => 'bare',
974             isa => HashRef[CodeRef],
975             handles_via => 'Hash',
976             handles => {
977             get_encoding => 'get',
978             add_encoding => 'set',
979             },
980             lazy => 1,
981             default => sub ($self) {
982             +{
983             identity => sub ($content_ref) { $content_ref },
984             base64 => sub ($content_ref) {
985             die "invalid characters\n"
986             if $content_ref->$* =~ m{[^A-Za-z0-9+/=]} or $content_ref->$* =~ m{=(?=[^=])};
987             require MIME::Base64; \ MIME::Base64::decode_base64($content_ref->$*);
988             },
989             base64url => sub ($content_ref) {
990             die "invalid characters\n"
991             if $content_ref->$* =~ m{[^A-Za-z0-9=_-]} or $content_ref->$* =~ m{=(?=[^=])};
992             require MIME::Base64; \ MIME::Base64::decode_base64url($content_ref->$*);
993             },
994             };
995             },
996             );
997              
998             # callback hook for Sereal::Encode
999 1     1 0 3 sub FREEZE ($self, $serializer) {
  1         2  
  1         2  
  1         3  
1000 1         9 my $data = +{ %$self };
1001             # Cpanel::JSON::XS doesn't serialize: https://github.com/Sereal/Sereal/issues/266
1002             # coderefs can't serialize cleanly and must be re-added by the user.
1003 1         6 delete $data->@{qw(_json_decoder _format_validations _media_type _encoding)};
1004 1         3 return $data;
1005             }
1006              
1007             # callback hook for Sereal::Decode
1008 1     1 0 10326 sub THAW ($class, $serializer, $data) {
  1         3  
  1         2  
  1         2  
  1         2  
1009 1         2 my $self = bless($data, $class);
1010              
1011             # load all vocabulary classes
1012 1         28 require_module($_) foreach uniq map $_->{vocabularies}->@*, $self->_canonical_resources;
1013              
1014 1         221 return $self;
1015             }
1016              
1017             1;
1018              
1019             __END__
1020              
1021             =pod
1022              
1023             =encoding UTF-8
1024              
1025             =for stopwords schema subschema metaschema validator evaluator listref
1026              
1027             =head1 NAME
1028              
1029             JSON::Schema::Modern - Validate data against a schema
1030              
1031             =head1 VERSION
1032              
1033             version 0.571
1034              
1035             =head1 SYNOPSIS
1036              
1037             use JSON::Schema::Modern;
1038              
1039             $js = JSON::Schema::Modern->new(
1040             specification_version => 'draft2020-12',
1041             output_format => 'flag',
1042             ... # other options
1043             );
1044             $result = $js->evaluate($instance_data, $schema_data);
1045              
1046             =head1 DESCRIPTION
1047              
1048             This module aims to be a fully-compliant L<JSON Schema|https://json-schema.org/> evaluator and
1049             validator, targeting the currently-latest
1050             L<Draft 2020-12|https://json-schema.org/specification-links.html#2020-12>
1051             version of the specification.
1052              
1053             =head1 CONFIGURATION OPTIONS
1054              
1055             These values are all passed as arguments to the constructor.
1056              
1057             =head2 specification_version
1058              
1059             Indicates which version of the JSON Schema specification is used during evaluation. When not set,
1060             this value is derived from the C<$schema> keyword in the schema used in evaluation, or defaults to
1061             the latest version (currently C<draft2020-12>).
1062              
1063             The use of this option is I<HIGHLY> encouraged to ensure continued correct operation of your schema.
1064             The current default value will not stay the same over time.
1065              
1066             May be one of:
1067              
1068             =over 4
1069              
1070             =item *
1071              
1072             L<C<draft2020-12> or C<2020-12>|https://json-schema.org/specification-links.html#2020-12>, corresponding to metaschema C<https://json-schema.org/draft/2020-12/schema>
1073              
1074             =item *
1075              
1076             L<C<draft2019-09> or C<2019-09>|https://json-schema.org/specification-links.html#2019-09-formerly-known-as-draft-8>, corresponding to metaschema C<https://json-schema.org/draft/2019-09/schema>
1077              
1078             =item *
1079              
1080             L<C<draft7> or C<7>|https://json-schema.org/specification-links.html#draft-7>, corresponding to metaschema C<http://json-schema.org/draft-07/schema#>
1081              
1082             =back
1083              
1084             Note that you can also use a C<$schema> keyword in the schema itself, to specify a different metaschema or
1085             specification version.
1086              
1087             =head2 output_format
1088              
1089             One of: C<flag>, C<basic>, C<strict_basic>, C<detailed>, C<verbose>, C<terse>. Defaults to C<basic>.
1090             C<strict_basic> can only be used with C<specification_version = draft2019-09>.
1091             Passed to L<JSON::Schema::Modern::Result/output_format>.
1092              
1093             =head2 short_circuit
1094              
1095             When true, evaluation will return early in any execution path as soon as the outcome can be
1096             determined, rather than continuing to find all errors or annotations.
1097             This option is safe to use in all circumstances, even in the presence of
1098             C<unevaluatedItems> and C<unevaluatedProperties> keywords: the validation result will not change;
1099             only some errors will be omitted from the result.
1100              
1101             Defaults to true when C<output_format> is C<flag>, and false otherwise.
1102              
1103             =head2 max_traversal_depth
1104              
1105             The maximum number of levels deep a schema traversal may go, before evaluation is halted. This is to
1106             protect against accidental infinite recursion, such as from two subschemas that each reference each
1107             other, or badly-written schemas that could be optimized. Defaults to 50.
1108              
1109             =head2 validate_formats
1110              
1111             When true, the C<format> keyword will be treated as an assertion, not merely an annotation. Defaults
1112             to true when specification_version is draft7, and false otherwise.
1113              
1114             =head2 format_validations
1115              
1116             =for stopwords subref
1117              
1118             An optional hashref that allows overriding the validation method for formats, or adding new ones.
1119             Overrides to existing formats (see L</Format Validation>)
1120             must be specified in the form of C<< { $format_name => $format_sub } >>, where
1121             the format sub is a subref that takes one argument and returns a boolean result. New formats must
1122             be specified in the form of C<< { $format_name => { type => $type, sub => $format_sub } } >>,
1123             where the type indicates which of the core JSON Schema types (null, object, array, boolean, string,
1124             number, or integer) the instance value must be for the format validation to be considered.
1125              
1126             =head2 validate_content_schemas
1127              
1128             When true, the C<contentMediaType> and C<contentSchema> keywords are not treated as pure annotations:
1129             C<contentEncoding> (when present) is used to decode the applied data payload and then
1130             C<contentMediaType> will be used as the media-type for decoding to produce the data payload which is
1131             then applied to the schema in C<contentSchema> for validation. (Note that treating these keywords as
1132             anything beyond simple annotations is contrary to the specification, therefore this option defaults
1133             to false.)
1134              
1135             See L</add_media_type> and L</add_encoding> for adding additional type support.
1136              
1137             =for stopwords shhh
1138              
1139             Technically only draft7 allows this and drafts 2019-09 and 2020-12 prohibit ever returning the
1140             subschema evaluation results together with their parent schema's results, so shhh. I'm trying to get this
1141             fixed for the next draft.
1142              
1143             =head2 collect_annotations
1144              
1145             When true, annotations are collected from keywords that produce them, when validation succeeds.
1146             These annotations are available in the returned result (see L<JSON::Schema::Modern::Result>).
1147             Defaults to false.
1148              
1149             =head2 scalarref_booleans
1150              
1151             When true, any value that is expected to be a boolean B<in the instance data> may also be expressed
1152             as the scalar references C<\0> or C<\1> (which are serialized as booleans by JSON backends).
1153              
1154             Defaults to false.
1155              
1156             =head2 stringy_numbers
1157              
1158             When true, any value that is expected to be a number or integer B<in the instance data> may also be
1159             expressed as a string. This does B<not> apply to the C<const> or C<enum> keywords, but only
1160             the following keywords:
1161              
1162             =over 4
1163              
1164             =item *
1165              
1166             C<type> (where both C<string> and C<number> (and possibly C<integer>) are considered valid
1167              
1168             =item *
1169              
1170             C<multipleOf>
1171              
1172             =item *
1173              
1174             C<maximum>
1175              
1176             =item *
1177              
1178             C<exclusiveMaximum>
1179              
1180             =item *
1181              
1182             C<minimum>
1183              
1184             =item *
1185              
1186             C<exclusiveMinimum>
1187              
1188             =back
1189              
1190             This allows you to write a schema like this (which validates a string representing an integer):
1191              
1192             type: string
1193             pattern: ^[0-9]$
1194             multipleOf: 4
1195             minimum: 16
1196             maximum: 256
1197              
1198             Such keywords are only applied if the value looks like a number, and do not generate a failure
1199             otherwise. Values are determined to be numbers via L<perlapi/looks_like_number>.
1200             This option is only intended to be used for evaluating data from sources that can only be strings,
1201             such as the extracted value of an HTTP header or query parameter.
1202              
1203             Defaults to false.
1204              
1205             =head2 strict
1206              
1207             When true, unrecognized keywords are disallowed in schemas (they will cause an immediate abort
1208             in L</traverse> or L</evaluate>).
1209              
1210             =head1 METHODS
1211              
1212             =for Pod::Coverage BUILDARGS FREEZE THAW
1213              
1214             =head2 evaluate_json_string
1215              
1216             $result = $js->evaluate_json_string($data_as_json_string, $schema);
1217             $result = $js->evaluate_json_string($data_as_json_string, $schema, { collect_annotations => 1});
1218              
1219             Evaluates the provided instance data against the known schema document.
1220              
1221             The data is in the form of a JSON-encoded string (in accordance with
1222             L<RFC8259|https://datatracker.ietf.org/doc/html/rfc8259>). B<The string is expected to be UTF-8 encoded.>
1223              
1224             The schema must be in one of these forms:
1225              
1226             =over 4
1227              
1228             =item *
1229              
1230             a Perl data structure, such as what is returned from a JSON decode operation,
1231              
1232             =item *
1233              
1234             a L<JSON::Schema::Modern::Document> object,
1235              
1236             =item *
1237              
1238             or a URI string indicating the location where such a schema is located.
1239              
1240             =back
1241              
1242             Optionally, a hashref can be passed as a third parameter which allows changing the values of the
1243             L</short_circuit>, L</collect_annotations>, L</scalarref_booleans>,
1244             L</stringy_numbers>, L</strict>, L</validate_formats>, and/or L</validate_content_schemas>
1245             settings for just this evaluation call.
1246              
1247             You can also pass use these keys to alter behaviour (these are generally only used by custom validation
1248             applications that contain embedded JSON Schemas):
1249              
1250             =over 4
1251              
1252             =item *
1253              
1254             C<data_path>: adjusts the effective path of the data instance as of the start of evaluation
1255              
1256             =item *
1257              
1258             C<traversed_schema_path>: adjusts the accumulated path as of the start of evaluation (or last C<$id> or C<$ref>)
1259              
1260             =item *
1261              
1262             C<initial_schema_uri>: adjusts the recorded absolute keyword location as of the start of evaluation
1263              
1264             =item *
1265              
1266             C<effective_base_uri>: locations in errors and annotations are resolved against this URI
1267              
1268             =back
1269              
1270             The return value is a L<JSON::Schema::Modern::Result> object, which can also be used as a boolean.
1271              
1272             =head2 evaluate
1273              
1274             $result = $js->evaluate($instance_data, $schema);
1275             $result = $js->evaluate($instance_data, $schema, { short_circuit => 0 });
1276              
1277             Evaluates the provided instance data against the known schema document.
1278              
1279             The data is in the form of an unblessed nested Perl data structure representing any type that JSON
1280             allows: null, boolean, string, number, object, array. (See L</TYPES> below.)
1281              
1282             The schema must be in one of these forms:
1283              
1284             =over 4
1285              
1286             =item *
1287              
1288             a Perl data structure, such as what is returned from a JSON decode operation,
1289              
1290             =item *
1291              
1292             a L<JSON::Schema::Modern::Document> object,
1293              
1294             =item *
1295              
1296             or a URI string indicating the location where such a schema is located.
1297              
1298             =back
1299              
1300             Optionally, a hashref can be passed as a third parameter which allows changing the values of the
1301             L</short_circuit>, L</collect_annotations>, L</scalarref_booleans>,
1302             L</stringy_numbers>, L</strict>, L</validate_formats>, and/or L</validate_content_schemas>
1303             settings for just this evaluation call.
1304              
1305             You can also pass use these keys to alter behaviour (these are generally only used by custom validation
1306             applications that contain embedded JSON Schemas):
1307              
1308             =over 4
1309              
1310             =item *
1311              
1312             C<data_path>: adjusts the effective path of the data instance as of the start of evaluation
1313              
1314             =item *
1315              
1316             C<traversed_schema_path>: adjusts the accumulated path as of the start of evaluation (or last C<$id> or C<$ref>)
1317              
1318             =item *
1319              
1320             C<initial_schema_uri>: adjusts the recorded absolute keyword location as of the start of evaluation
1321              
1322             =item *
1323              
1324             C<effective_base_uri>: locations in errors and annotations are resolved against this URI
1325              
1326             =back
1327              
1328             You can pass a series of callback subs to this method corresponding to keywords, which is useful for
1329             identifying various data that are not exposed by annotations.
1330             This feature is highly experimental and may change in the future.
1331              
1332             For example, to find the locations where all C<$ref> keywords are applied B<successfully>:
1333              
1334             my @used_ref_at;
1335             $js->evaluate($data, $schema_or_uri, {
1336             callbacks => {
1337             '$ref' => sub ($data, $schema, $state) {
1338             push @used_ref_at, $state->{data_path};
1339             }
1340             },
1341             });
1342              
1343             The return value is a L<JSON::Schema::Modern::Result> object, which can also be used as a boolean.
1344             Callbacks are not compatible with L</short_circuit> mode.
1345              
1346             =head2 validate_schema
1347              
1348             $result = $js->validate_schema($schema);
1349              
1350             Evaluates the provided schema as instance data against its metaschema. Accepts C<$schema> and
1351             C<$config_override> parameters in the same form as L</evaluate>.
1352              
1353             =head2 traverse
1354              
1355             $result = $js->traverse($schema);
1356             $result = $js->traverse($schema, { initial_schema_uri => 'http://example.com' });
1357              
1358             Traverses the provided schema without evaluating it against any instance data. Returns the
1359             internal state object accumulated during the traversal, including any identifiers found therein, and
1360             any errors found during parsing. For internal purposes only.
1361              
1362             Optionally, a hashref can be passed as a second parameter which alters some
1363             behaviour (these are generally only used by custom validation
1364             applications that contain embedded JSON Schemas):
1365              
1366             =over 4
1367              
1368             =item *
1369              
1370             C<traversed_schema_path>: adjusts the accumulated path as of the start of evaluation (or last C<$id> or C<$ref>)
1371              
1372             =item *
1373              
1374             C<initial_schema_uri>: adjusts the recorded absolute keyword location as of the start of evaluation
1375              
1376             =item *
1377              
1378             C<metaschema_uri>: use the indicated URI as the metaschema
1379              
1380             =back
1381              
1382             You can pass a series of callback subs to this method corresponding to keywords, which is useful for
1383             extracting data from within schemas and skipping properties that may look like keywords but actually
1384             are not (for example C<{"const":{"$ref": "this is not actually a $ref"}}>). This feature is highly
1385             experimental and is highly likely to change in the future.
1386              
1387             For example, to find the resolved targets of all C<$ref> keywords in a schema document:
1388              
1389             my @refs;
1390             JSON::Schema::Modern->new->traverse($schema, {
1391             callbacks => {
1392             '$ref' => sub ($schema, $state) {
1393             push @refs, Mojo::URL->new($schema->{'$ref'})
1394             ->to_abs(JSON::Schema::Modern::Utilities::canonical_uri($state));
1395             }
1396             },
1397             });
1398              
1399             =head2 add_schema
1400              
1401             $js->add_schema($uri => $schema);
1402             $js->add_schema($uri => $document);
1403             $js->add_schema($schema);
1404             $js->add_schema($document);
1405              
1406             Introduces the (unblessed, nested) Perl data structure or L<JSON::Schema::Modern::Document>
1407             object, representing a JSON Schema, to the implementation, registering it under the indicated URI if
1408             provided (and if not, C<''> will be used if no other identifier can be found within).
1409              
1410             You B<MUST> call C<add_schema> for any external resources that a schema may reference via C<$ref>
1411             before calling L</evaluate>, other than the standard metaschemas which are loaded from a local cache
1412             as needed.
1413              
1414             Returns C<undef> if the resource could not be found;
1415             if there were errors in the document, will die with these errors;
1416             otherwise returns the L<JSON::Schema::Modern::Document> that contains the added schema.
1417              
1418             =head2 add_format_validation
1419              
1420             =for comment we are the nine Eleven Deniers
1421              
1422             $js->add_format_validation(no_nines => { type => 'number', sub => sub ($value) { $value =~ m/^[0-8]$$/ });
1423              
1424             Adds support for a custom format. The data type that this format applies to must be supplied; all
1425             values of any other type will automatically be deemed to be valid, and will not be passed to the
1426             subref.
1427              
1428             =head2 add_vocabulary
1429              
1430             $js->add_vocabulary('My::Custom::Vocabulary::Class');
1431              
1432             Makes a custom vocabulary class available to metaschemas that make use of this vocabulary.
1433             as described in the specification at
1434             L<"Meta-Schemas and Vocabularies"|https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.8.1>.
1435              
1436             The class must compose the L<JSON::Schema::Modern::Vocabulary> role and implement the
1437             L<vocabulary|JSON::Schema::Modern::Vocabulary/vocabulary> and
1438             L<keywords|JSON::Schema::Modern::Vocabulary/keywords> methods.
1439              
1440             =head2 add_media_type
1441              
1442             $js->add_media_type('application/furble' => sub ($content_ref) {
1443             return ...; # data representing the deserialized text for Content-Type: application/furble
1444             });
1445              
1446             Takes a media-type name and a subref which takes a single scalar reference, which is expected to be
1447             a reference to a string, which might contain wide characters (i.e. not octets), especially when used
1448             in conjunction with L</get_encoding> below. Must return B<a reference to a value of any type> (which is
1449             then dereferenced for the C<contentSchema> keyword).
1450              
1451             These media types are already known:
1452              
1453             =over 4
1454              
1455             =item *
1456              
1457             C<application/json> - see L<RFC 4627|https://datatracker.ietf.org/doc/html/rfc4627>
1458              
1459             =item *
1460              
1461             C<application/schema+json> - see L<proposed definition|https://json-schema.org/draft/2020-12/json-schema-core.html#name-application-schemajson>
1462              
1463             =item *
1464              
1465             C<application/schema-instance+json> - see L<proposed definition|https://json-schema.org/draft/2020-12/json-schema-core.html#name-application-schema-instance>
1466              
1467             =item *
1468              
1469             C<application/octet-stream> - passes strings through unchanged
1470              
1471             =item *
1472              
1473             C<application/x-www-form-urlencoded>
1474              
1475             =item *
1476              
1477             C<application/x-ndjson> - see L<https://github.com/ndjson/ndjson-spec>
1478              
1479             =item *
1480              
1481             C<text/*> - passes strings through unchanged
1482              
1483             =back
1484              
1485             =head2 get_media_type
1486              
1487             Fetches a decoder sub for the indicated media type. Lookups are performed B<without case sensitivity>.
1488              
1489             =for stopwords thusly
1490              
1491             You can use it thusly:
1492              
1493             $js->add_media_type('application/furble' => sub { ... }); # as above
1494             my $decoder = $self->get_media_type('application/furble') or die 'cannot find media type decoder';
1495             my $content_ref = $decoder->(\$content_string);
1496              
1497             =head2 add_encoding
1498              
1499             $js->add_encoding('bloop' => sub ($content_ref) {
1500             return \ ...; # data representing the deserialized content for Content-Transfer-Encoding: bloop
1501             });
1502              
1503             Takes an encoding name and a subref which takes a single scalar reference, which is expected to be
1504             a reference to a string, which SHOULD be a 7-bit or 8-bit string. Result values MUST be a scalar-reference
1505             to a string (which is then dereferenced for the C<contentMediaType> keyword).
1506              
1507             =for stopwords natively
1508              
1509             Encodings handled natively are:
1510              
1511             =over 4
1512              
1513             =item *
1514              
1515             C<identity> - passes strings through unchanged
1516              
1517             =item *
1518              
1519             C<base64> - see L<RFC 4648 §4|https://datatracker.ietf.org/doc/html/rfc4648#section-4>
1520              
1521             =item *
1522              
1523             C<base64url> - see L<RFC 4648 §5|https://datatracker.ietf.org/doc/html/rfc4648#section-5>
1524              
1525             =back
1526              
1527             See also L<HTTP::Message/encode>.
1528              
1529             =head2 get_encoding
1530              
1531             Fetches a decoder sub for the indicated encoding. Incoming values MUST be a reference to an octet
1532             string. Result values will be a scalar-reference to a string, which might be passed to a media_type
1533             decoder (see above).
1534              
1535             You can use it thusly:
1536              
1537             my $decoder = $self->get_encoding('base64') or die 'cannot find encoding decoder';
1538             my $content_ref = $decoder->(\$content_string);
1539              
1540             =head2 get
1541              
1542             my $schema = $js->get($uri);
1543             my ($schema, $canonical_uri) = $js->get($uri);
1544              
1545             Fetches the Perl data structure representing the JSON Schema at the indicated URI. When called in
1546             list context, the canonical URI of that location is also returned, as a L<Mojo::URL>. Returns
1547             C<undef> if the schema with that URI has not been loaded (or cached).
1548              
1549             =head1 LIMITATIONS
1550              
1551             =head2 Types
1552              
1553             Perl is a more loosely-typed language than JSON. This module delves into a value's internal
1554             representation in an attempt to derive the true "intended" type of the value. However, if a value is
1555             used in another context (for example, a numeric value is concatenated into a string, or a numeric
1556             string is used in an arithmetic operation), additional flags can be added onto the variable causing
1557             it to resemble the other type. This should not be an issue if data validation is occurring
1558             immediately after decoding a JSON payload, or if the JSON string itself is passed to this module.
1559             If you are still having difficulties, make sure you are using Perl's fastest and most trusted and
1560             reliable JSON decoder, L<Cpanel::JSON::XS> (or its proxy, useful for fatpacking, L<JSON::MaybeXS>).
1561             Other JSON decoders are known to produce data with incorrect data types.
1562              
1563             For more information, see L<Cpanel::JSON::XS/MAPPING>.
1564              
1565             =head2 Format Validation
1566              
1567             By default (and unless you specify a custom metaschema with the C<$schema> keyword or
1568             L<JSON::Schema::Modern::Document/metaschema>),
1569             formats are treated only as annotations, not assertions. When L</validate_formats> is
1570             true, strings are also checked against the format as specified in the schema. At present the
1571             following formats are supported (use of any other formats than these will always evaluate as true,
1572             but remember you can always supply custom format handlers; see L</format_validations> above):
1573              
1574             =over 4
1575              
1576             =item *
1577              
1578             C<date-time>
1579              
1580             =item *
1581              
1582             C<date>
1583              
1584             =item *
1585              
1586             C<time>
1587              
1588             =item *
1589              
1590             C<duration>
1591              
1592             =item *
1593              
1594             C<email>
1595              
1596             =item *
1597              
1598             C<idn-email>
1599              
1600             =item *
1601              
1602             C<hostname>
1603              
1604             =item *
1605              
1606             C<idn-hostname>
1607              
1608             =item *
1609              
1610             C<ipv4>
1611              
1612             =item *
1613              
1614             C<ipv6>
1615              
1616             =item *
1617              
1618             C<uri>
1619              
1620             =item *
1621              
1622             C<uri-reference>
1623              
1624             =item *
1625              
1626             C<iri>
1627              
1628             =item *
1629              
1630             C<uuid>
1631              
1632             =item *
1633              
1634             C<json-pointer>
1635              
1636             =item *
1637              
1638             C<relative-json-pointer>
1639              
1640             =item *
1641              
1642             C<regex>
1643              
1644             =back
1645              
1646             A few optional prerequisites are needed for some of these (if the prerequisite is missing,
1647             validation will always succeed):
1648              
1649             =over 4
1650              
1651             =item *
1652              
1653             C<date-time>, C<date>, and C<time> require L<Time::Moment>, L<DateTime::Format::RFC3339>
1654              
1655             =item *
1656              
1657             C<email> and C<idn-email> require L<Email::Address::XS> version 1.04 (or higher)
1658              
1659             =item *
1660              
1661             C<hostname> and C<idn-hostname> require L<Data::Validate::Domain>
1662              
1663             =item *
1664              
1665             C<idn-hostname> requires L<Net::IDN::Encode>
1666              
1667             =back
1668              
1669             =head2 Specification Compliance
1670              
1671             This implementation is now fully specification-compliant (for versions draft7, draft2019-09,
1672             draft2020-12), but until version 1.000 is released, it is
1673             still deemed to be missing some optional but quite useful features, such as:
1674              
1675             =over 4
1676              
1677             =item *
1678              
1679             loading schema documents from disk
1680              
1681             =item *
1682              
1683             loading schema documents from the network
1684              
1685             =item *
1686              
1687             loading schema documents from a local web application (e.g. L<Mojolicious>)
1688              
1689             =item *
1690              
1691             additional output formats beyond C<flag>, C<basic>, and C<terse> (L<https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.12>)
1692              
1693             =back
1694              
1695             =head1 SECURITY CONSIDERATIONS
1696              
1697             The C<pattern> and C<patternProperties> keywords evaluate regular expressions from the schema,
1698             the C<regex> format validator evaluates regular expressions from the data, and some keywords
1699             in the Validation vocabulary perform floating point operations on potentially-very large numbers.
1700             No effort is taken (at this time) to sanitize the regular expressions for embedded code or
1701             detect potentially pathological constructs that may pose a security risk, either via denial of
1702             service or by allowing exposure to the internals of your application. B<DO NOT USE SCHEMAS FROM
1703             UNTRUSTED SOURCES.>
1704              
1705             =head1 SEE ALSO
1706              
1707             =over 4
1708              
1709             =item *
1710              
1711             L<json-schema-eval>
1712              
1713             =item *
1714              
1715             L<https://json-schema.org>
1716              
1717             =item *
1718              
1719             L<RFC8259: The JavaScript Object Notation (JSON) Data Interchange Format|https://datatracker.ietf.org/doc/html/rfc8259>
1720              
1721             =item *
1722              
1723             L<RFC3986: Uniform Resource Identifier (URI): Generic Syntax|https://datatracker.ietf.org/doc/html/rfc3986>
1724              
1725             =item *
1726              
1727             L<Test::JSON::Schema::Acceptance>: contains the official JSON Schema test suite
1728              
1729             =item *
1730              
1731             L<JSON::Schema::Tiny>: a more stripped-down implementation of the specification, with fewer dependencies and faster evaluation
1732              
1733             =item *
1734              
1735             L<https://json-schema.org/draft/2020-12/release-notes.html>
1736              
1737             =item *
1738              
1739             L<https://json-schema.org/draft/2019-09/release-notes.html>
1740              
1741             =item *
1742              
1743             L<https://json-schema.org/draft-07/json-schema-release-notes.html>
1744              
1745             =item *
1746              
1747             L<Understanding JSON Schema|https://json-schema.org/understanding-json-schema>: tutorial-focused documentation
1748              
1749             =back
1750              
1751             =for stopwords OpenAPI
1752              
1753             =head1 SUPPORT
1754              
1755             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Modern/issues>.
1756              
1757             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
1758              
1759             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
1760             server|https://open-api.slack.com>, which are also great resources for finding help.
1761              
1762             =head1 AUTHOR
1763              
1764             Karen Etheridge <ether@cpan.org>
1765              
1766             =head1 COPYRIGHT AND LICENCE
1767              
1768             This software is copyright (c) 2020 by Karen Etheridge.
1769              
1770             This is free software; you can redistribute it and/or modify it under
1771             the same terms as the Perl 5 programming language system itself.
1772              
1773             =cut