File Coverage

blib/lib/JSON/Schema/Modern.pm
Criterion Covered Total %
statement 370 377 98.1
branch 131 156 83.9
condition 80 94 85.1
subroutine 65 67 97.0
pod 12 14 85.7
total 658 708 92.9


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