File Coverage

lib/JSON/Schema/Validate.pm
Criterion Covered Total %
statement 1162 1813 64.0
branch 682 1262 54.0
condition 225 609 36.9
subroutine 119 139 85.6
pod 30 30 100.0
total 2218 3853 57.5


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## JSON Schema Validator - ~/lib/JSON/Schema/Validate.pm
3             ## Version v0.9.0
4             ## Copyright(c) 2026 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2025/11/07
7             ## Modified 2026/03/01
8             ## All rights reserved
9             ##
10             ##
11             ## This program is free software; you can redistribute it and/or modify it
12             ## under the same terms as Perl itself.
13             ##----------------------------------------------------------------------------
14             package JSON::Schema::Validate;
15             BEGIN
16             {
17 44     44   5994602 use strict;
  44         91  
  44         1806  
18 44     44   202 use warnings;
  44         81  
  44         2883  
19 44     44   249 use warnings::register;
  44         80  
  44         2795  
20 44     44   246 use vars qw( $VERSION $DEBUG );
  44         105  
  44         2912  
21 44     44   275 use B ();
  44         101  
  44         619  
22 44     44   1198 use JSON ();
  44         17080  
  44         991  
23 44     44   192 use Scalar::Util qw( blessed looks_like_number reftype refaddr );
  44         64  
  44         3787  
24 44     44   270 use List::Util qw( first any all );
  44         111  
  44         3301  
25 44     44   22759 use Encode ();
  44         873141  
  44         1909  
26 44     44   1091 our $VERSION = 'v0.9.0';
27             };
28              
29 44     44   650 use v5.16.0;
  44         149  
30 44     44   204 use strict;
  44         59  
  44         903  
31 44     44   150 use warnings;
  44         63  
  44         49985  
32              
33             sub new
34             {
35 121     121 1 7256051 my $class = shift( @_ );
36 121         263 my $schema = shift( @_ );
37 121 50 33     1179 if( defined( $schema ) && ( !ref( $schema ) || ref( $schema ) ne 'HASH' ) )
      33        
38             {
39 0         0 die( "You provided a value (", overload::StrVal( $schema ), "), but it is not an hash reference. You must create a new $class object like this: $class->new( \$schema, \%opts );" );
40             }
41              
42 121         750 my $self =
43             {
44             comment_handler => undef,
45             # boolean
46             compile_on => 0,
47             # { schema, anchors, id_index, base }
48             compiled => undef,
49             # boolean; when 0, failures don’t invalidate; when 1, they do
50             content_assert => 0,
51             content_decoders => {},
52             errors => [],
53             # boolean; when true, then non-standard extensions are enabled.
54             extensions => 0,
55             formats => {},
56             # boolean
57             ignore_req_vocab => 0,
58             last_error => '',
59             last_trace => [],
60             max_errors => 200,
61             media_validators => {},
62             # boolean
63             normalize_instance => 1,
64             # boolean: when true, prune unknown properties before validate()
65             prune_unknown => 0,
66             # ($abs_uri) -> $schema_hashref
67             resolver => undef,
68             schema => _clone( $schema ),
69             # 0 = unlimited
70             trace_limit => 0,
71             # boolean
72             trace_on => 0,
73             # 0 = record all
74             trace_sample => 0,
75             # boolean; when true, 'uniqueKeys' extension is enabled.
76             unique_keys => 0,
77             # internal boolean; not an option
78             vocab_checked => 0,
79             vocab_support => {},
80             };
81              
82 121         422 bless( $self, $class );
83 121         572 my $opts = $self->_get_args_as_hash( @_ );
84 121         535 my @bool_options = qw(
85             content_assert
86             extensions
87             ignore_req_vocab
88             normalize_instance
89             prune_unknown
90             unique_keys
91             );
92 121         302 foreach my $opt ( @bool_options )
93             {
94 726 100       1577 next unless( exists( $opts->{ $opt } ) );
95 8 50       35 $self->{ $opt } = $opts->{ $opt } ? 1 : 0
96             }
97             # Make sure the boolean value for 'extensions' is propagated to 'unique_keys' unless the option 'unique_keys' has been explicitly specified, and then we do not want to overwrite it.
98 121 50       696 $self->{unique_keys} = $self->{extensions} unless( exists( $opts->{unique_keys} ) );
99 121 100       594 if( exists( $opts->{ignore_unknown_required_vocab} ) )
100             {
101 1 50       6 $self->{ignore_req_vocab} = $opts->{ignore_unknown_required_vocab} ? 1 : 0;
102             }
103 121 100       516 if( exists( $opts->{compile} ) )
104             {
105 11 100       49 $self->{compile_on} = $opts->{compile} ? 1 : 0;
106             }
107 121 100       489 if( exists( $opts->{trace} ) )
108             {
109 6 50       16 $self->{trace_on} = $opts->{trace} ? 1 : 0;
110             }
111              
112 121         419 my @other_options = qw( max_errors trace_limit );
113 121         260 foreach my $opt ( @other_options )
114             {
115 242 100       605 next unless( exists( $opts->{ $opt } ) );
116 7         19 $self->{ $opt } = $opts->{ $opt };
117             }
118              
119 121 100       337 if( exists( $opts->{trace_sample} ) )
120             {
121             # Check for percentage integer (0 to 100)
122 1 50       8 if( $opts->{trace_sample} =~ /^([0-9]{1,2}|100)$/ )
123             {
124 1         3 $self->{trace_sample} = $opts->{trace_sample};
125             }
126             else
127             {
128 0 0       0 warn( "Warning only: invalid value for option 'trace_sample'." ) if( warnings::enabled() );
129             }
130             }
131              
132             # User-supplied format callbacks (override precedence left to caller order)
133 121 100 66     505 if( $opts->{format} && ref( $opts->{format} ) eq 'HASH' )
134             {
135 3         21 $self->{formats}->{ $_ } = $opts->{format}->{ $_ } for( keys( %{$opts->{format}} ) );
  3         23  
136             }
137 121 100       541 $self->{vocab_support} = $opts->{vocab_support} ? { %{$opts->{vocab_support}} } : {};
  1         4  
138              
139 121 100       694 $self->_check_vocabulary_required unless( $self->{ignore_req_vocab} );
140 120 100       391 $self->_register_builtin_media_validators() if( $self->{content_assert} );
141 120 100       352 $self->{compiled} = _compile_root( $self->{schema} ) if( $self->{compile_on} );
142 120         606 return( $self );
143             }
144              
145             # $js->compile -> enables it
146             # $js->compile(1) -> enables it
147             # $js->compile(0) -> disables it
148             sub compile
149             {
150 1     1 1 626 my( $self, $bool ) = @_;
151 1 50       5 my $on = defined( $bool ) ? $bool : 1;
152 1         2 $self->{compile_on} = $on;
153              
154 1 50 33     8 if( $self->{compile_on} && !$self->{compiled} )
155             {
156 1         5 $self->{compiled} = _compile_root( $self->{schema} );
157             }
158 1         3 return( $self );
159             }
160              
161             sub compile_js
162             {
163 0     0 1 0 my $self = shift( @_ );
164 0         0 my $opts = $self->_get_args_as_hash( @_ );
165              
166             my $schema = $self->{schema}
167 0 0       0 or die( "No schema loaded; cannot compile to JavaScript" );
168              
169             # Public JS API name, e.g. "validateIncorporation"
170             my $name = exists( $opts->{name} ) && defined( $opts->{name} ) && length( $opts->{name} )
171             ? $opts->{name}
172 0 0 0     0 : 'validate';
173              
174             # Max errors to collect on the client side
175 0 0       0 my $max_errors = exists( $opts->{max_errors} ) ? int( $opts->{max_errors} ) : 200;
176 0 0       0 $max_errors = 0 if( $max_errors < 0 );
177              
178 0         0 my %seen; # schema_pointer -> function name
179 0         0 my $counter = 0; # for generating unique function names
180 0         0 my @funcs; # accumulated JS validator functions
181              
182 0         0 my $root_ptr = '#';
183              
184             # Pass $opts down so we can see ecma => ... inside the compiler.
185 0         0 my $root_fn = $self->_compile_js_node( $schema, $root_ptr, \%seen, \@funcs, \$counter, $schema, $opts );
186              
187 0         0 my $js = '';
188              
189 0         0 $js .= <<'JS_RUNTIME';
190             (function(global)
191             {
192             "use strict";
193              
194             function _jsv_err(ctx, path, keyword, message, schemaPtr)
195             {
196             if(ctx.maxErrors && ctx.errors.length >= ctx.maxErrors)
197             {
198             return;
199             }
200             ctx.errors.push({
201             path: path,
202             keyword: keyword,
203             message: message,
204             schema_pointer: schemaPtr
205             });
206             }
207              
208             function _jsv_typeOf(x)
209             {
210             if(x === null)
211             {
212             return "null";
213             }
214             if(Array.isArray ? Array.isArray(x) : Object.prototype.toString.call(x) === "[object Array]")
215             {
216             return "array";
217             }
218             var t = typeof x;
219             if(t === "number" && isFinite(x) && Math.floor(x) === x)
220             {
221             // distinguish "integer" for convenience
222             return "integer";
223             }
224             return t;
225             }
226              
227             function _jsv_hasOwn(obj, prop)
228             {
229             return Object.prototype.hasOwnProperty.call(obj, prop);
230             }
231              
232             JS_RUNTIME
233              
234             # Public entry point
235 0         0 $js .= <<"JS_RUNTIME";
236             function $name(instance)
237             {
238             var ctx = { errors: [], maxErrors: $max_errors };
239             $root_fn(instance, "#", ctx);
240             return ctx.errors;
241             }
242              
243             JS_RUNTIME
244              
245             # Attach to global (browser: window) in a conservative way
246 0         0 $js .= <<"JS_RUNTIME";
247             if(typeof global === 'object' && global)
248             {
249             global.$name = $name;
250             }
251              
252             JS_RUNTIME
253              
254             # Emit all compiled validator functions, indented one level
255 0         0 $js .= join( "\n\n", map{ ' ' . join( "\n ", split( /\n/, $_ ) ) } @funcs );
  0         0  
256              
257 0         0 $js .= <<'JS_RUNTIME';
258              
259             })(this);
260             JS_RUNTIME
261              
262 0         0 return( $js );
263             }
264              
265             # $js->content_checks -> enables it
266             # $js->content_checks(1) -> enables it
267             # $js->content_checks(0) -> disables it
268             sub content_checks
269             {
270 3     3 1 24 my( $self, $bool ) = @_;
271 3 50       10 my $on = defined( $bool ) ? $bool : 1;
272 3 50       11 $self->{content_assert} = $on ? 1 : 0;
273 3 50       23 $self->_register_builtin_media_validators() if( $self->{content_assert} );
274 3         4 return( $self );
275             }
276              
277             # TODO: Backward compatibility, but need to remove it
278             {
279 44     44   351 no warnings 'once';
  44         74  
  44         1271313  
280             *enable_content_checks = \&content_checks;
281             }
282              
283 21     21 1 953 sub error { $_[0]->{last_error} }
284              
285             # We return a copy of the array reference containing the error objects
286 3     3 1 403 sub errors { return( [@{$_[0]->{errors}}] ); }
  3         12  
287              
288             sub extensions
289             {
290 1     1 1 3 my( $self, $bool ) = @_;
291 1 50       4 my $on = defined( $bool ) ? $bool : 1;
292 1         3 $self->{extensions} = $on;
293 1         4 $self->unique_keys( $on );
294 1         23 return( $self );
295             }
296              
297             sub get_trace
298             {
299 3     3 1 508 my( $self ) = @_;
300 3 50       7 return( [@{ $self->{last_trace} || [] }] );
  3         20  
301             }
302              
303             # Accessor-only method. See trace_limit for its mutator alter ego.
304 0   0 0 1 0 sub get_trace_limit { 0 + ( $_[0]->{trace_limit} // 0 ) }
305              
306             # $js->ignore_unknown_required_vocab -> enables it
307             # $js->ignore_unknown_required_vocab(1) -> enables it
308             # $js->ignore_unknown_required_vocab(0) -> disables it
309             sub ignore_unknown_required_vocab
310             {
311 0     0 1 0 my( $self, $bool ) = @_;
312 0 0       0 my $on = defined( $bool ) ? $bool : 1;
313 0         0 $self->{ignore_req_vocab} = $on;
314 0         0 return( $self );
315             }
316              
317 0 0   0 1 0 sub is_compile_enabled { $_[0]->{compile_on} ? 1 : 0 }
318              
319 0 0   0 1 0 sub is_content_checks_enabled { $_[0]->{content_assert} ? 1 : 0 }
320              
321             # Accessor only method. See trace or the mutator vession.
322 0 0   0 1 0 sub is_trace_on { $_[0]->{trace_on} ? 1 : 0 }
323              
324 0 0   0 1 0 sub is_unique_keys_enabled { $_[0]->{unique_keys} ? 1 : 0 }
325              
326 0 0   0 1 0 sub is_unknown_required_vocab_ignored { $_[0]->{ignore_req_vocab} ? 1 : 0 }
327              
328             sub is_valid
329             {
330 2     2 1 6628 my $self = shift( @_ );
331 2         4 my $data = shift( @_ );
332              
333 2         7 my $opts = $self->_get_args_as_hash( @_ );
334             # Optional: allow overriding max_errors for this call only
335             # (e.g. $v->is_valid( $data, max_errors => 1 );)
336 2   50     29 $opts->{max_errors} //= 1;
337              
338             # validate already populates $self->{errors}; is_valid just returns boolean
339 2 100       8 return( $self->validate( $data, $opts ) ? 1 : 0 );
340             }
341              
342             # Example:
343             # my $pruned = $js->prune_instance( $incoming_data );
344             sub prune_instance
345             {
346 5     5 1 56 my( $self, $data ) = @_;
347              
348             # Work on a cloned copy if normalize_instance is on,
349             # to remain consistent with validate().
350 5 50       12 if( $self->{normalize_instance} )
351             {
352 5         14 $data = _clone( $data );
353             }
354              
355 5         21 return( $self->_prune_with_schema( $self->{schema}, $data ) );
356             }
357              
358             sub prune_unknown
359             {
360 0     0 1 0 my( $self, $bool ) = @_;
361 0 0       0 my $on = defined( $bool ) ? $bool : 1;
362 0 0       0 $self->{prune_unknown} = $on ? 1 : 0;
363 0         0 return( $self );
364             }
365              
366             sub register_builtin_formats
367             {
368 47     47 1 87 my( $self ) = @_;
369              
370 47         71 local $@;
371 47 50       94 my $has_iso = eval{ require DateTime::Format::ISO8601; 1 } ? 1 : 0;
  47         6539  
  0         0  
372 47 50       135 my $has_tp = eval{ require Time::Piece; 1 } ? 1 : 0;
  47         4716  
  47         95311  
373 47 50       76 my $has_dt = eval{ require DateTime; 1 } ? 1 : 0;
  47         7684  
  47         4325591  
374 47 50       122 my $has_idn = eval{ require Net::IDN::Encode; 1 } ? 1 : 0;
  47         7018  
  0         0  
375             # perl -MRegexp::Common=Email::Address -lE 'say $Regexp::Common::RE{Email}{Address}'
376 47         190 state $email_re = qr/\A(?:(?^u:(?:(?^u:(?>(?^u:(?^u:(?>(?^u:(?>(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*))|\.|\s*"(?^u:(?^u:[^\\"])|(?^u:\\(?^u:[^\x0A\x0D])))+"\s*))+))|(?>(?^u:(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*))|(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*"(?^u:(?^u:[^\\"])|(?^u:\\(?^u:[^\x0A\x0D])))*"(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*)))+))?)(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*<(?^u:(?^u:(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*(?^u:(?>[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+(?:\.[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+)*))(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*))|(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*"(?^u:(?^u:[^\\"])|(?^u:\\(?^u:[^\x0A\x0D])))*"(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*)))\@(?^u:(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*(?^u:(?>[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+(?:\.[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+)*))(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*))|(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*\[(?:\s*(?^u:(?^u:[^\[\]\\])|(?^u:\\(?^u:[^\x0A\x0D]))))*\s*\](?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*))))>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*)))|(?^u:(?^u:(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*(?^u:(?>[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+(?:\.[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+)*))(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*))|(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*"(?^u:(?^u:[^\\"])|(?^u:\\(?^u:[^\x0A\x0D])))*"(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*)))\@(?^u:(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*(?^u:(?>[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+(?:\.[^\x00-\x1F\x7F()<>\[\]:;@\\,."\s]+)*))(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*))|(?^u:(?>(?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*\[(?:\s*(?^u:(?^u:[^\[\]\\])|(?^u:\\(?^u:[^\x0A\x0D]))))*\s*\](?^u:(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))|(?>\s+))*)))))(?>(?^u:(?>\s*\((?:\s*(?^u:(?^u:(?>[^()\\]+))|(?^u:\\(?^u:[^\x0A\x0D]))|))*\s*\)\s*))*)))\z/;
377              
378 47         88 my %F;
379              
380             # RFC3339 date-time / date / time
381             $F{'date-time'} = sub
382             {
383 2     2   4 my( $s ) = @_;
384 2 50 33     47 return(0) unless( defined( $s ) && !ref( $s ) );
385              
386             # Preferred path when DateTime::Format::ISO8601 is available
387 2 50       6 if( $has_iso )
388             {
389 0 0       0 return( eval{ DateTime::Format::ISO8601->parse_datetime( $s ) ? 1 : 0 } ? 1 : 0 );
  0 0       0  
390             }
391              
392             # YYYY-MM-DDThh:mm:ss[.fraction](Z|±hh:mm)
393 2 50       24 return(0) unless( $s =~ /\A
394             (\d{4})-(\d{2})-(\d{2}) # date
395             T
396             (\d{2}):(\d{2}):(\d{2}) # time
397             (?:\.\d+)? # optional fraction
398             (?:Z|[+\-]\d{2}:\d{2}) # offset
399             \z/x );
400              
401 2         28 my( $y, $m, $d, $H, $M, $S ) = ( $1, $2, $3, $4, $5, $6 );
402              
403             # Time::Piece is a core module (available since perl 5.10): use it first
404 2 50       5 if( $has_tp )
405             {
406             # strptime validates calendar correctness (e.g. rejects Feb 30)
407             # We feed it the bare datetime without offset/fraction (already captured above)
408 2         15 my $bare = sprintf( '%04d-%02d-%02dT%02d:%02d:%02d', $y, $m, $d, $H, $M, $S );
409 2 100       5 return( eval{ Time::Piece->strptime( $bare, '%Y-%m-%dT%H:%M:%S' ); 1 } ? 1 : 0 );
  2         20  
  1         152  
410             }
411              
412             # Fall back to DateTime if available
413 0 0       0 if( $has_dt )
414             {
415             return( eval
416 0 0       0 {
417 0         0 DateTime->new(
418             year => $y,
419             month => $m,
420             day => $d,
421             hour => $H,
422             minute => $M,
423             second => $S
424             );
425 0         0 1
426             } ? 1 : 0 );
427             }
428              
429             # Last resort: the regex above was already sufficient for structural checks
430 0         0 return(1);
431 47         422 };
432              
433             $F{'date'} = sub
434             {
435 2     2   5 my( $s ) = @_;
436 2 50 33     12 return(0) unless( defined( $s ) && !ref( $s ) );
437 2 50       15 return(0) unless( $s =~ /\A(\d{4})-(\d{2})-(\d{2})\z/ );
438 2         13 my( $y, $m, $d ) = ( $1, $2, $3 );
439              
440             # Time::Piece is core — prefer it
441 2 50       7 if( $has_tp )
442             {
443 2 100       3 return( eval{ Time::Piece->strptime( $s, '%Y-%m-%d' ); 1 } ? 1 : 0 );
  2         14  
  1         69  
444             }
445              
446             # Fall back to DateTime if available
447 0 0       0 if( $has_dt )
448             {
449 0 0       0 return( eval{ DateTime->new( year => $y, month => $m, day => $d ); 1 } ? 1 : 0 );
  0         0  
  0         0  
450             }
451              
452             # Last resort: structural regex already passed above
453 0         0 return(1);
454 47         263 };
455              
456             $F{'time'} = sub
457             {
458 2     2   5 my( $s ) = @_;
459 2 50 33     11 return(0) unless( defined( $s ) && !ref( $s ) );
460 2 50       20 if( $has_iso )
461             {
462 0 0       0 return eval{ DateTime::Format::ISO8601->parse_datetime( "1970-01-01T$s" ) ? 1 : 0 } ? 1 : 0;
  0 0       0  
463             }
464 2 100       18 return $s =~ /\A
465             (?:[01]\d|2[0-3]) # HH
466             :
467             (?:[0-5]\d) # MM
468             :
469             (?:[0-5]\d) # SS
470             (?:\.\d+)? # .fraction
471             (?:Z|[+\-](?:[01]\d|2[0-3]):[0-5]\d)? # offset
472             \z/x ? 1 : 0;
473 47         200 };
474              
475             # Duration (RFC 3339 §5.6 / ISO 8601)
476             # Valid: P1Y, P3M, P1Y2M3DT4H5M6S, PT10S, P1W
477             # Invalid: P, PT, P1Y2M3DT (T with nothing after it, or no designator at all)
478             $F{'duration'} = sub
479             {
480 2     2   5 my( $s ) = @_;
481 2 50 33     27 return(0) unless( defined( $s ) && !ref( $s ) );
482              
483             # Week form: PnW (stands alone; no mixing with other designators per ISO 8601)
484 2 50       9 return(1) if( $s =~ /\AP\d+W\z/ );
485              
486             # Full form: at least one date *or* time component must be present
487             # The T separator must be followed by at least one time designator
488 2 100 66     36 return( $s =~ /\A
489             P
490             (?:(\d+)Y)?
491             (?:(\d+)M)?
492             (?:(\d+)D)?
493             (?:T
494             (?=\d) # T must be followed by at least one digit
495             (?:(\d+)H)?
496             (?:(\d+)M)?
497             (?:(\d+(?:\.\d+)?)S)?
498             )?
499             \z/x && ( defined($1) || defined($2) || defined($3) || defined($4) || defined($5) || defined($6) )
500             ? 1 : 0 );
501 47         195 };
502              
503             # Email / IDN email
504             # Plain email (ASCII) — unchanged
505             $F{'email'} = sub
506             {
507 13     13   27 my( $s ) = @_;
508 13 50 33     75 return(0) unless( defined( $s ) && !ref( $s ) );
509 13 100       454 return( $s =~ $email_re ? 1 : 0 );
510 47         205 };
511              
512             # IDN email: punycode the domain, validate with same regex
513             $F{'idn-email'} = sub
514             {
515 1     1   3 my( $s ) = @_;
516 1 50 33     20 return(0) unless( defined( $s ) && !ref( $s ) );
517 1 50       10 return(0) unless( $s =~ /\A(.+)\@(.+)\z/s ); # keep local-part as-is (EAI allows UTF-8)
518 1         6 my( $local, $domain ) = ( $1, $2 );
519              
520 1 50       4 if( $has_idn )
521             {
522 0         0 local $@;
523 0         0 my $ascii = eval{ Net::IDN::Encode::domain_to_ascii( $domain ) };
  0         0  
524 0 0 0     0 return(0) unless( defined( $ascii ) && length( $ascii ) );
525              
526 0         0 my $candidate = "$local\@$ascii";
527 0 0       0 return( $candidate =~ $email_re ? 1 : 0 );
528             }
529              
530             # Fallback: if the domain already *looks* ASCII, validate directly
531 1 50       7 if( $domain =~ /\A[[:ascii:]]+\z/ )
532             {
533 1         3 my $candidate = "$local\@$domain";
534 1 50       24 return( $candidate =~ $email_re ? 1 : 0 );
535             }
536              
537             # Without IDN module, fall back to permissive Unicode domain check + ASCII regex
538 0         0 return(0);
539 47         232 };
540              
541             # Hostnames
542             $F{'hostname'} = sub
543             {
544 2     2   6 my( $s ) = @_;
545 2 50 33     10 return(0) unless( defined( $s ) && !ref( $s ) );
546 2 50       22 return(0) if( length( $s ) > 253 );
547 2         9 for my $label ( split( /\./, $s ) )
548             {
549 3 50 33     19 return(0) unless( length( $label ) >= 1 && length( $label ) <= 63 );
550 3 100       21 return(0) unless( $label =~ /\A[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\z/ );
551             }
552 1         4 return(1);
553 47         247 };
554              
555             $F{'idn-hostname'} = sub
556             {
557 1     1   3 my( $s ) = @_;
558 1 50 33     34 return(0) unless( defined( $s ) && !ref( $s ) );
559 1 50       5 if( $has_idn )
560             {
561 0         0 local $@;
562 0         0 my $ascii = eval{ Net::IDN::Encode::domain_to_ascii( $s ) };
  0         0  
563 0 0 0     0 return(0) unless( defined( $ascii ) && length( $ascii ) );
564 0 0       0 return( $F{'hostname'}->( $ascii ) ? 1 : 0 );
565             }
566              
567             # Fallback: permissive Unicode label check (as you had), then ASCII hostname rule
568 1 50       14 return(0) if( length( $s ) > 253 );
569 1         6 for my $label ( split( /\./, $s ) )
570             {
571 2 50 33     11 return(0) unless( length( $label ) >= 1 && length( $label ) <= 63 );
572 2 50       12 return(0) unless( $label =~ /\A[[:alnum:]\pL\pN](?:[[:alnum:]\pL\pN\-]*[[:alnum:]\pL\pN])?\z/u );
573             }
574 1         5 return(1);
575 47         353 };
576              
577             # IP addresses
578             $F{'ipv4'} = sub
579             {
580 2     2   5 my( $s ) = @_;
581 2 50 33     11 return(0) unless( defined( $s ) && !ref( $s ) );
582 2 100       44 return $s =~ /\A
583             (25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.
584             (25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.
585             (25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.
586             (25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)
587             \z/x ? 1 : 0;
588 47         216 };
589              
590             $F{'ipv6'} = sub
591             {
592 2     2   4 my( $s ) = @_;
593 2 50 33     26 return(0) unless( defined( $s ) && !ref( $s ) );
594 2 100       33 return $s =~ /\A
595             (?: (?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}
596             | (?:[0-9A-Fa-f]{1,4}:){1,7}:
597             | (?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}
598             | (?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}
599             | (?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}
600             | (?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}
601             | (?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}
602             | [0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6})
603             | :(?:(?::[0-9A-Fa-f]{1,4}){1,7}|:)
604             | (?:[0-9A-Fa-f]{1,4}:){6}
605             (?:\d{1,3}\.){3}\d{1,3}
606             | ::(?:[0-9A-Fa-f]{1,4}:){0,5}
607             (?:\d{1,3}\.){3}\d{1,3}
608             | (?:[0-9A-Fa-f]{1,4}:){1,5}:
609             (?:\d{1,3}\.){3}\d{1,3}
610             )
611             \z/x ? 1 : 0;
612 47         314 };
613              
614             # URI/IRI
615             $F{'uri'} = sub
616             {
617 2     2   6 my( $s ) = @_;
618 2 50 33     48 return(0) unless( defined( $s ) && !ref( $s ) );
619 2 100       16 return( $s =~ /\A[A-Za-z][A-Za-z0-9+\-.]*:[^\s]+\z/ ? 1 : 0 );
620 47         355 };
621              
622             $F{'uri-reference'} = sub
623             {
624 1     1   3 my( $s ) = @_;
625 1 50 33     21 return(0) unless( defined( $s ) && !ref( $s ) );
626 1 50       10 return( $s =~ /\A(?:[A-Za-z][A-Za-z0-9+\-.]*:)?[^\s]+\z/ ? 1 : 0 );
627 47         265 };
628              
629             $F{'iri'} = sub
630             {
631 1     1   3 my( $s ) = @_;
632 1 50 33     8 return(0) unless( defined( $s ) && !ref( $s ) );
633             # Must have a scheme (letters followed by colon) per RFC 3987 §2.2
634             # Scheme: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
635             # After scheme: at least one non-whitespace character
636 1 50       13 return( $s =~ /\A[A-Za-z][A-Za-z0-9+\-.]*:[^\s]+\z/u ? 1 : 0 );
637 47         251 };
638              
639             # UUID
640             $F{'uuid'} = sub
641             {
642 3     3   7 my( $s ) = @_;
643 3 50 33     18 return(0) unless( defined( $s ) && !ref( $s ) );
644 3 100       19 return( $s =~ /\A[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12}\z/ ? 1 : 0 );
645 47         193 };
646              
647             # JSON Pointer / Relative JSON Pointer
648             $F{'json-pointer'} = sub
649             {
650 3     3   8 my( $s ) = @_;
651 3 50 33     14 return(0) unless( defined( $s ) && !ref( $s ) );
652 3 100       11 return(1) if( $s eq '' );
653 2 100       21 return( $s =~ m{\A/(?:[^~/]|~[01])*(?:/(?:[^~/]|~[01])*)*\z} ? 1 : 0 );
654 47         222 };
655              
656             $F{'relative-json-pointer'} = sub
657             {
658 3     3   9 my( $s ) = @_;
659 3 50 33     18 return(0) unless( defined( $s ) && !ref( $s ) );
660 3 100       13 return(1) if( $s =~ /\A0\z/ );
661 2 100       35 return( $s =~ m,\A[1-9]\d*(?:#|(?:/(?:[^~/]|~[01])*)*)\z, ? 1 : 0 );
662 47         247 };
663              
664             # Regex
665             $F{'regex'} = sub
666             {
667 2     2   6 my( $s ) = @_;
668 2 50 33     11 return(0) unless( defined( $s ) && !ref( $s ) );
669 2         4 local $@;
670 2 100       3 return( eval{ "" =~ /$s/; 1 } ? 1 : 0 );
  2         101  
  1         8  
671 47         241 };
672              
673 47         255 while( my( $k, $v ) = each( %F ) )
674             {
675 799 50       3157 $self->{formats}->{ $k } = $v unless( exists( $self->{formats}->{ $k } ) );
676             }
677              
678 47         262 return( $self );
679             }
680              
681             sub register_content_decoder
682             {
683 1     1 1 11 my( $self, $name, $cb ) = @_;
684 1 50       3 if( ref( $cb ) eq 'CODE' )
685             {
686 1         3 $self->{content_decoders}->{ lc( "$name" ) } = $cb;
687             }
688             else
689             {
690 0         0 die( "content decoder must be a code reference" );
691             }
692 1         2 return( $self );
693             }
694              
695             sub register_format
696             {
697 0     0 1 0 my( $self, $name, $code ) = @_;
698 0 0 0     0 die( "format name required" ) unless( defined( $name ) && length( $name ) );
699 0 0       0 die( "format validator must be a coderef" ) unless( ref( $code ) eq 'CODE' );
700 0         0 $self->{formats}->{ $name } = $code;
701 0         0 return( $self );
702             }
703              
704             sub register_media_validator
705             {
706 5     5 1 20 my( $self, $type, $cb ) = @_;
707 5 50       35 if( ref( $cb ) eq 'CODE' )
708             {
709 5         27 $self->{media_validators}->{ lc( "$type" ) } = $cb;
710             }
711             else
712             {
713 0         0 die( "media validator must be a code reference" );
714             }
715 5         10 return( $self );
716             }
717              
718             sub set_comment_handler
719             {
720 4     4 1 37 my( $self, $code ) = @_;
721 4 50       16 if( @_ > 1 )
722             {
723 4 100 100     27 if( defined( $code ) && ref( $code ) ne 'CODE' )
724             {
725 1 50       150 warn( "Warning only: the handler provided is not a code reference." ) if( warnings::enabled() );
726 1         10 return( $self );
727             }
728 3         9 $self->{comment_handler} = $code;
729             }
730 3         13 return( $self );
731             }
732              
733             sub set_resolver
734             {
735 2     2 1 12 my( $self, $code ) = @_;
736 2 50       9 if( @_ > 1 )
737             {
738 2 50 33     12 if( defined( $code ) && ref( $code ) ne 'CODE' )
739             {
740 0 0       0 warn( "Warning only: the handler provided is not a code reference." ) if( warnings::enabled() );
741 0         0 return( $self );
742             }
743 2         4 $self->{resolver} = $code;
744             }
745 2         5 return( $self );
746             }
747              
748             sub set_vocabulary_support
749             {
750 0     0 1 0 my( $self, $h ) = @_;
751 0 0       0 $self->{vocab_support} = { %{ $h || {} } };
  0         0  
752 0         0 $self->{vocab_checked} = 0;
753 0         0 return( $self );
754             }
755              
756             # This is a mutator only method
757             # $js->trace -> enables it
758             # $js->trace(1) -> enables it
759             # $js->trace(0) -> disables it
760             # Always returns the object
761             # See is_trace_on for the accessor method
762             sub trace
763             {
764 1     1 1 1200 my( $self, $bool ) = @_;
765 1 50       5 my $on = defined( $bool ) ? $bool : 1;
766 1         3 $self->{trace_on} = $on;
767 1         3 return( $self );
768             }
769              
770             sub trace_limit
771             {
772 0     0 1 0 my( $self, $n ) = @_;
773 0   0     0 $self->{trace_limit} = 0 + ( $n || 0 );
774 0         0 return( $self );
775             }
776              
777             sub trace_sample
778             {
779 0     0 1 0 my( $self, $pct ) = @_;
780 0   0     0 $self->{trace_sample} = 0 + ( $pct || 0 );
781 0         0 return( $self );
782             }
783              
784             sub unique_keys
785             {
786 9     9 1 32 my( $self, $bool ) = @_;
787 9 50       18 my $on = defined( $bool ) ? $bool : 1;
788 9         13 $self->{unique_keys} = $on;
789 9         24 return( $self );
790             }
791              
792             sub validate
793             {
794 218     218 1 44169 my $self = shift( @_ );
795 218         409 my $data = shift( @_ );
796 218         743 my $opts = $self->_get_args_as_hash( @_ );
797              
798 218         609 $self->{errors} = [];
799 218         479 $self->{last_error} = '';
800              
801             # Ensure we have the compiled root (indexing/anchors) even in lazy mode
802 218 100       1106 $self->{compiled} = _compile_root( $self->{schema} ) unless( $self->{compiled} );
803              
804             # One-time $vocabulary check (Draft 2020-12)
805 218 50       685 if( !$self->{vocab_checked} )
806             {
807 0         0 my $root = $self->{schema};
808 0 0 0     0 if( ref( $root ) eq 'HASH' &&
809             ref( $root->{'$vocabulary'} ) eq 'HASH' )
810             {
811 0         0 my $decl = $root->{'$vocabulary'}; # { uri => JSON::true|false, ... }
812 0   0     0 my $support = $self->{vocab_support} || {};
813 0         0 for my $uri ( keys( %$decl ) )
814             {
815 0 0       0 next unless( $decl->{ $uri } ); # only enforce for required=true
816 0 0       0 next if( $support->{ $uri } ); # caller says it's supported
817 0 0       0 unless( $self->{ignore_req_vocab} )
818             {
819 0         0 die( "Required vocabulary not supported: $uri" );
820             }
821             }
822             }
823 0         0 $self->{vocab_checked} = 1;
824             }
825              
826             # Because Perl scalar are not JSON scalar, we force the Perl structure into strict JSON types, eliminating all Perl duality and guaranteeing predictable validation semantics.
827 218 50       548 if( $self->{normalize_instance} )
828             {
829 218         596 $data = _clone( $data );
830             }
831              
832             # Optional pre-validation pruning of unknown properties / nested objects.
833             # This only happens if explicitly enabled via prune_unknown => 1.
834 218 100       665 if( $self->{prune_unknown} )
835             {
836 1         5 $data = $self->_prune_with_schema( $self->{schema}, $data );
837             }
838              
839             my $ctx =
840             {
841             root => $self->{compiled},
842             instance_root => $data,
843             resolver => $self->{resolver},
844             formats => $self->{formats},
845             errors => $self->{errors},
846             max_errors => ( ( defined( $opts->{max_errors} ) && $opts->{max_errors} =~ /^\d+$/ ) ? $opts->{max_errors} : $self->{max_errors} ),
847             error_count => 0,
848              
849             # paths / recursion
850             ptr_stack => ['#'],
851             id_stack => [ $self->{compiled}->{base} ],
852             dyn_stack => [ {} ], # dynamicAnchor scope frames
853             visited => {}, # "schema_ptr|inst_addr" => 1
854              
855             # annotation (for unevaluated*)
856             ann_mode => 1,
857             compile_on => ( defined( $opts->{compile_on} ) ? ( $opts->{compile_on} ? 1 : 0 ) : ( $self->{compile_on} ? 1 : 0 ) ),
858              
859             # trace
860             trace_on => ( defined( $opts->{trace_on} ) ? ( $opts->{trace_on} ? 1 : 0 ) : ( $self->{trace_on} ? 1 : 0 ) ),
861             trace_sample => $self->{trace_sample} || 0,
862             trace_limit => ( defined( $opts->{trace_limit} ) && $opts->{trace_limit} =~ /^\d+$/ ) ? $opts->{trace_limit} : ( $self->{trace_limit} || 0 ),
863             trace => [],
864              
865             # content assertion & helpers
866             content_assert => ( defined( $opts->{content_assert} ) ? ( $opts->{content_assert} ? 1 : 0 ) : ( $self->{content_assert} ? 1 : 0 ) ),
867             media_validators => $self->{media_validators},
868             content_decoders => $self->{content_decoders},
869              
870             comment_handler => $self->{comment_handler},
871              
872             # extensions
873             unique_keys => $self->{unique_keys},
874             extensions => $self->{extensions},
875 218 100 66     7325 };
    0 100        
    100 33        
    50 100        
    0          
    100          
    50          
    50          
    0          
    100          
    50          
876              
877             # Guarantee at least one trace entry when trace is enabled
878 218 100       688 if( $ctx->{trace_on} )
879             {
880 6         10 push( @{$ctx->{trace}},
  6         43  
881             {
882             schema_ptr => '#',
883             keyword => 'validate',
884             inst_path => '#',
885             outcome => 'start',
886             note => 'start',
887             });
888             }
889              
890 218         723 my $res = _v( $ctx, '#', $self->{compiled}->{schema}, $data );
891              
892 218         599 $self->{last_trace} = $ctx->{trace};
893              
894 218 100       541 if( !$res->{ok} )
895             {
896             # $self->{last_error} = _first_error_text( $self->{errors} );
897 106 50       1621 $self->{last_error} = scalar( @{$self->{errors}} ) ? $self->{errors}->[0] : '';
  106         452  
898 106         1287 return(0);
899             }
900 112         1447 return(1);
901             }
902              
903             sub _apply_dynamic_ref
904             {
905 5     5   23 my( $ctx, $schema_ptr, $ref, $inst ) = @_;
906              
907 5 50       59 if( $ref =~ /\#(.+)\z/ )
908             {
909 5         17 my $name = $1;
910 5         8 for( my $i = $#{ $ctx->{dyn_stack} }; $i >= 0; $i-- )
  5         24  
911             {
912 5         9 my $frame = $ctx->{dyn_stack}->[$i];
913 5 50       15 if( my $node = $frame->{ $name } )
914             {
915 5         18 my $sp = _ptr_of_node( $ctx->{root}, $node );
916 5         22 return( _v( $ctx, $sp, $node, $inst ) );
917             }
918             }
919             }
920              
921 0         0 return( _apply_ref( $ctx, $schema_ptr, $ref, $inst ) );
922             }
923              
924             # $ref and $dynamicRef
925             sub _apply_ref
926             {
927 22     22   83 my( $ctx, $schema_ptr, $ref, $inst ) = @_;
928              
929 22         42 my $base = $ctx->{id_stack}->[-1];
930 22         59 my $abs = _resolve_uri( $base, $ref );
931              
932             # Direct absolute ID/anchor hit
933 22 100       83 if( my $node = $ctx->{root}->{id_index}->{ $abs } )
934             {
935 3         10 my $sp = _ptr_of_node( $ctx->{root}, $node );
936 3         10 return( _v( $ctx, $sp, $node, $inst ) );
937             }
938              
939             # Local fragment
940 19 100       62 if( $ref =~ /^\#/ )
941             {
942             # 1) Try anchors (e.g. "#foo" / "#MyAnchor")
943 14 100       53 if( my $n = $ctx->{root}->{anchors}->{ $ref } )
944             {
945 5         27 return( _v( $ctx, $ref, $n, $inst ) );
946             }
947 9 50       35 if( $ref =~ /^\#([A-Za-z0-9._-]+)\z/ )
948             {
949 0         0 my $cand = $base . '#' . $1;
950 0 0       0 if( my $node = $ctx->{root}->{id_index}->{ $cand } )
951             {
952 0         0 my $sp = _ptr_of_node( $ctx->{root}, $node );
953 0         0 return( _v( $ctx, $sp, $node, $inst ) );
954             }
955             }
956             # 2) If it’s a JSON Pointer ( "#/..." ), use _jsv_resolve_internal_ref
957 9 50       29 if( $ref =~ m{\A\#/(?:[^~/]|~[01])} )
958             {
959             # or pulled from id_index; same one used in compile_js
960 9 50       17 my $root_schema = $ctx->{root}->{schema} or return( _err_res( $ctx, $schema_ptr, "missing schema", '$ref' ) );
961              
962 9         18 my $node = _jsv_resolve_internal_ref( $root_schema, $ref );
963 9 100       14 if( $node )
964             {
965 8         14 return( _v( $ctx, $ref, $node, $inst ) );
966             }
967              
968 1         6 return _err_res(
969             $ctx,
970             $schema_ptr,
971             "unresolved JSON Pointer fragment in \$ref: $abs",
972             '$ref'
973             );
974             }
975             # 3) If not a JSON Pointer and not an anchor, fall through to external resolver / error
976             }
977              
978             # External resolver hook
979 5 50       12 if( $ctx->{resolver} )
980             {
981 5         9 local $@;
982 5         8 my $ext = eval{ $ctx->{resolver}->( $abs ) };
  5         18  
983 5 50 33     71 return( _err_res( $ctx, $schema_ptr, "resolver failed for \$ref: $abs", '$ref' ) ) unless( $ext && ref( $ext ) );
984              
985 5 50 33     38 my $ext_base = _normalize_uri( ( ref( $ext ) eq 'HASH' && $ext->{'$id'} ) ? $ext->{'$id'} : $abs );
986              
987 5         26 my( $frag ) = ( $abs =~ /(\#.*)\z/ );
988 5         11 my ( $anchors, $ids ) = ( {}, {} );
989 5         15 _index_schema_202012( $ext, $ext_base, '#', $anchors, $ids );
990              
991 5         8 push( @{$ctx->{id_stack}}, $ext_base );
  5         14  
992              
993             # If the abs URI included a JSON Pointer fragment, honor it here
994 5         8 my $target_ptr = '#';
995 5 50 33     32 if( defined( $frag ) && length( $frag ) )
996             {
997 5 50       20 if( $frag =~ m{\A\#/(?:[^~/]|~[01])} )
    0          
998             {
999             # JSON Pointer fragment
1000 5 50       40 if( my $node = $anchors->{ $frag } )
1001             {
1002 5         20 my $r = _v( $ctx, $frag, $node, $inst );
1003 5         8 pop( @{$ctx->{id_stack}} );
  5         9  
1004 5         40 return( $r );
1005             }
1006             else
1007             {
1008 0         0 pop( @{$ctx->{id_stack}} );
  0         0  
1009 0         0 return( _err_res( $ctx, $schema_ptr, "unresolved JSON Pointer fragment in \$ref: $abs", '$ref' ) );
1010             }
1011             }
1012             elsif( $frag =~ /\A\#([A-Za-z0-9._-]+)\z/ )
1013             {
1014 0         0 my $cand = $ext_base . '#' . $1;
1015 0 0       0 if( my $node = $ids->{ $cand } )
1016             {
1017 0   0     0 my $sp = _ptr_of_node( { anchors => $anchors }, $node ) || '#';
1018 0         0 my $r = _v( $ctx, $sp, $node, $inst );
1019 0         0 pop( @{$ctx->{id_stack}} );
  0         0  
1020 0         0 return( $r );
1021             }
1022             }
1023             }
1024              
1025 0         0 my $r = _v( $ctx, '#', $ext, $inst );
1026 0         0 pop( @{$ctx->{id_stack}} );
  0         0  
1027 0         0 return( $r );
1028             }
1029              
1030 0         0 return( _err_res( $ctx, $schema_ptr, "unresolved \$ref: $ref (abs: $abs)", '$ref' ) );
1031             }
1032              
1033             sub _canon
1034             {
1035 86     86   112 my( $v ) = @_;
1036 86         181 state $json = JSON->new->allow_nonref(1)->canonical(1)->convert_blessed(1);
1037 86         311 return( $json->encode( $v ) );
1038             }
1039              
1040             sub _check_vocabulary_required
1041             {
1042 120     120   293 my( $self ) = @_;
1043 120 50       423 return(1) if( $self->{vocab_checked} );
1044              
1045 120         270 my $root = $self->{schema};
1046 120 100 66     987 if( ref( $root ) eq 'HASH' && ref( $root->{'$vocabulary'} ) eq 'HASH' )
1047             {
1048 2         4 my $decl = $root->{'$vocabulary'};
1049 2   50     6 my $support = $self->{vocab_support} || {};
1050 2         19 for my $uri ( keys( %$decl ) )
1051             {
1052             # required only
1053 3 100       50 next unless( $decl->{ $uri } );
1054 1 50       6 next if( $support->{ $uri } );
1055             # TODO: Maybe we should return an exception rather than dying; it would be more user-friendly
1056 1         18 die( "Required vocabulary not supported: $uri" );
1057             }
1058             }
1059 119         285 return( $self->{vocab_checked} = 1 );
1060             }
1061              
1062             sub _clone
1063             {
1064 344     344   690 my( $v ) = @_;
1065 344         1813 state $json = JSON->new->allow_nonref(1)->canonical(1);
1066 344         8086 return( $json->decode( $json->encode( $v ) ) );
1067             }
1068              
1069             sub _compile_js_node
1070             {
1071 0     0   0 my $self = shift( @_ );
1072 0         0 my( $S, $sp, $seen, $funcs, $counter_ref, $root, $opts ) = @_;
1073              
1074 0   0     0 $opts ||= {};
1075 0 0       0 my $ecma = exists( $opts->{ecma} ) ? $opts->{ecma} : 'auto';
1076 0   0     0 my $force_unicode =
1077             defined( $ecma ) &&
1078             $ecma =~ /^\d+$/ &&
1079             $ecma >= 2018;
1080              
1081             # Re-use same JS function for the same schema pointer
1082 0 0       0 if( exists( $seen->{ $sp } ) )
1083             {
1084 0         0 return( $seen->{ $sp } );
1085             }
1086              
1087             # Support pointer-alias schemas such as:
1088             # definitions => { address => "#/definitions/jp_address" }
1089 0 0 0     0 if( defined( $S ) && !ref( $S ) && $S =~ /^#\// )
      0        
1090             {
1091 0         0 my $target_ptr = $S;
1092              
1093             # If we already compiled that pointer, just alias it
1094 0 0       0 if( exists( $seen->{ $target_ptr } ) )
1095             {
1096 0         0 $seen->{ $sp } = $seen->{ $target_ptr };
1097 0         0 return( $seen->{ $target_ptr } );
1098             }
1099              
1100 0         0 my $target_schema = _jsv_resolve_internal_ref( $root, $target_ptr );
1101              
1102             # Follow chains of pointer-aliases
1103 0 0 0     0 if( defined( $target_schema ) && !ref( $target_schema ) )
1104             {
1105 0         0 my %followed;
1106 0   0     0 while(
      0        
1107             defined( $target_schema ) &&
1108             !ref( $target_schema ) &&
1109             $target_schema =~ /^#\//
1110             )
1111             {
1112 0 0       0 last if( $followed{ $target_schema }++ );
1113 0         0 $target_ptr = $target_schema;
1114 0         0 $target_schema = _jsv_resolve_internal_ref( $root, $target_ptr );
1115             }
1116             }
1117              
1118 0 0       0 unless( defined( $target_schema ) )
1119             {
1120             # Could not resolve: emit a no-op validator (server still enforces)
1121 0         0 my $id = $$counter_ref++;
1122 0         0 my $fn = "jsv_node_${id}";
1123 0         0 $seen->{ $sp } = $fn;
1124 0         0 push( @$funcs, <<JS_RUNTIME );
1125             // $sp (unresolved pointer-alias $S)
1126             function $fn(inst, path, ctx)
1127             {
1128             }
1129             JS_RUNTIME
1130 0         0 return( $fn );
1131             }
1132              
1133 0 0       0 unless( ref( $target_schema ) eq 'HASH' )
1134             {
1135 0   0     0 die(
1136             "Internal error: pointer-alias '$S' at '$sp' did not resolve to an object schema " .
1137             "(got " . ( ref( $target_schema ) || 'scalar' ) . ")."
1138             );
1139             }
1140              
1141 0         0 my $base_fn = $self->_compile_js_node( $target_schema, $target_ptr, $seen, $funcs, $counter_ref, $root, $opts );
1142              
1143             # Alias this pointer to the target validator
1144 0         0 $seen->{ $sp } = $base_fn;
1145 0         0 return( $base_fn );
1146             }
1147              
1148             # NOTE: $ref
1149             # 0) $ref handling (internal refs only)
1150 0 0 0     0 if( ref( $S ) eq 'HASH' &&
1151             exists( $S->{'$ref'} ) )
1152             {
1153 0         0 my $ref = $S->{'$ref'};
1154              
1155             # Only support internal refs ("#/...") in JS
1156 0 0 0     0 if( defined( $ref ) && $ref =~ /^#/ )
1157             {
1158 0         0 my $target_ptr = $ref;
1159              
1160             # If we already compiled that pointer, just alias and return it
1161 0 0       0 if( exists( $seen->{ $target_ptr } ) )
1162             {
1163 0         0 $seen->{ $sp } = $seen->{ $target_ptr };
1164 0         0 return( $seen->{ $target_ptr } );
1165             }
1166              
1167             # Otherwise, resolve pointer against the root schema
1168 0         0 my $target_schema = _jsv_resolve_internal_ref( $root, $target_ptr );
1169              
1170              
1171             # Support "pointer alias" schemas such as:
1172             # definitions => { address => "#/definitions/jp_address" }
1173             # Keep resolving until we reach a real schema hash.
1174 0 0 0     0 if( defined( $target_schema ) && !ref( $target_schema ) )
1175             {
1176 0         0 my %followed;
1177 0   0     0 while(
      0        
1178             defined( $target_schema ) &&
1179             !ref( $target_schema ) &&
1180             $target_schema =~ /^#\//
1181             )
1182             {
1183 0 0       0 last if( $followed{ $target_schema }++ );
1184 0         0 $target_ptr = $target_schema;
1185 0         0 $target_schema = _jsv_resolve_internal_ref( $root, $target_ptr );
1186             }
1187             }
1188              
1189 0 0       0 unless( defined( $target_schema ) )
1190             {
1191             # Pointer could not be resolved. As a safety fallback,
1192             # make this node a no-op on the client (server will still enforce).
1193 0         0 my $id = $$counter_ref++;
1194 0         0 my $fn = "jsv_node_${id}";
1195 0         0 $seen->{ $sp } = $fn;
1196 0         0 push( @$funcs, <<JS_RUNTIME );
1197             // $sp
1198             function $fn(inst, path, ctx)
1199             {
1200             }
1201              
1202             JS_RUNTIME
1203 0         0 return( $fn );
1204             }
1205              
1206 0 0       0 unless( ref( $target_schema ) eq 'HASH' )
1207             {
1208 0 0 0     0 die( "Internal error: \$ref target '$target_ptr' did not resolve to a schema object (got " .
1209             ( defined( $target_schema ) ? ref( $target_schema ) || 'scalar' : 'undef' ) .
1210             "). If you are using pointer-alias definitions, they must ultimately resolve to an object schema." );
1211             }
1212              
1213             # Compile the target, then merge sibling keywords
1214 0         0 my $base_fn = $self->_compile_js_node( $target_schema, $target_ptr, $seen, $funcs, $counter_ref, $root, $opts );
1215              
1216             # Create a wrapper that runs both the referenced schema AND local keywords
1217 0         0 my $id = $$counter_ref++;
1218 0         0 my $wrapper_fn = "jsv_node_$id";
1219 0         0 $seen->{ $sp } = $wrapper_fn;
1220              
1221 0         0 my @wrapper_body;
1222 0         0 push( @wrapper_body, <<JS_RUNTIME );
1223             // $sp (\$ref + siblings)
1224             function $wrapper_fn(inst, path, ctx)
1225             {
1226             $base_fn(inst, path, ctx);
1227             if(ctx.errors.length >= ctx.maxErrors) return;
1228             JS_RUNTIME
1229              
1230             # Now compile the current node again, but skip the $ref
1231 0         0 my $local_S = { %$S }; # shallow copy
1232 0         0 delete( $local_S->{'$ref'} );
1233 0         0 my $local_sp = _join_ptr( $sp, '__local__' );
1234 0         0 my $local_fn = $self->_compile_js_node( $local_S, $local_sp, $seen, $funcs, $counter_ref, $root, $opts );
1235              
1236 0 0       0 if( $local_fn ne $wrapper_fn )
1237             {
1238 0         0 push( @wrapper_body, <<JS_RUNTIME );
1239             $local_fn(inst, path, ctx);
1240             JS_RUNTIME
1241             }
1242              
1243 0         0 push( @wrapper_body, <<JS_RUNTIME );
1244             }
1245             JS_RUNTIME
1246 0         0 push( @$funcs, join( '', @wrapper_body ) );
1247 0         0 return( $wrapper_fn );
1248             }
1249              
1250             # External refs (URLs, etc.) are not resolved on the client.
1251             # Full resolution is done server-side.
1252             }
1253              
1254 0         0 my $id = $$counter_ref++;
1255 0         0 my $fn = "jsv_node_$id";
1256 0         0 $seen->{ $sp } = $fn;
1257              
1258 0         0 my @body;
1259              
1260 0         0 push( @body, <<JS_RUNTIME );
1261             // $sp
1262             function $fn(inst, path, ctx)
1263             {
1264             JS_RUNTIME
1265              
1266             # NOTE: combinator (allOf / anyOf / oneOf / not / if-then-else)
1267 0 0       0 if( ref( $S ) eq 'HASH' )
1268             {
1269             # allOf – AND of subschemas
1270 0 0 0     0 if( exists( $S->{allOf} ) &&
      0        
1271             ref( $S->{allOf} ) eq 'ARRAY' &&
1272 0         0 @{$S->{allOf}} )
1273             {
1274 0         0 for my $i ( 0 .. $#{$S->{allOf}} )
  0         0  
1275             {
1276 0         0 my $sub_sp = _join_ptr( $sp, 'allOf', $i );
1277 0         0 my $sub_fn = $self->_compile_js_node( $S->{allOf}->[ $i ], $sub_sp, $seen, $funcs, $counter_ref, $root, $opts );
1278              
1279 0         0 push( @body, <<JS_RUNTIME );
1280             // $sub_sp
1281             $sub_fn(inst, path + '/allOf/$i', ctx);
1282             if(ctx.maxErrors && ctx.errors.length >= ctx.maxErrors) return;
1283             JS_RUNTIME
1284             }
1285              
1286             # IMPORTANT: we do *not* return here.
1287             # Other local keywords (type, properties, contains, ...) must still run.
1288             }
1289              
1290             # anyOf – at least one must validate (we only emit a single anyOf error)
1291 0 0 0     0 if( exists( $S->{anyOf} ) &&
      0        
1292             ref( $S->{anyOf} ) eq 'ARRAY' &&
1293 0         0 @{$S->{anyOf}} )
1294             {
1295 0         0 push( @body, <<JS_RUNTIME );
1296             (function()
1297             {
1298             var baseErrors = ctx.errors;
1299             var matched = false;
1300              
1301             JS_RUNTIME
1302              
1303 0         0 for my $i ( 0 .. $#{$S->{anyOf}} )
  0         0  
1304             {
1305 0         0 my $sub_sp = _join_ptr( $sp, 'anyOf', $i );
1306 0         0 my $sub_fn = $self->_compile_js_node( $S->{anyOf}->[ $i ], $sub_sp, $seen, $funcs, $counter_ref, $root, $opts );
1307              
1308 0         0 push( @body, <<JS_RUNTIME );
1309             if(matched) return;
1310             ctx.errors = [];
1311             // $sub_sp
1312             $sub_fn(inst, path + '/anyOf/$i', ctx);
1313             if(ctx.errors.length === 0)
1314             {
1315             matched = true;
1316             ctx.errors = baseErrors;
1317             return;
1318             }
1319             JS_RUNTIME
1320             }
1321              
1322 0         0 my $sp_qp = _js_quote( $sp );
1323 0         0 push( @body, <<JS_RUNTIME );
1324             ctx.errors = baseErrors;
1325             if(!matched)
1326             {
1327             _jsv_err(ctx, path, 'anyOf', 'no subschema matched', $sp_qp);
1328             }
1329             })();
1330             // return;
1331             JS_RUNTIME
1332             }
1333              
1334             # oneOf – exactly one must validate
1335 0 0 0     0 if( exists( $S->{oneOf} ) &&
      0        
1336             ref( $S->{oneOf} ) eq 'ARRAY' &&
1337 0         0 @{$S->{oneOf}} )
1338             {
1339 0         0 push( @body, <<JS_RUNTIME );
1340             (function()
1341             {
1342             var baseErrors = ctx.errors;
1343             var hits = 0;
1344              
1345             JS_RUNTIME
1346              
1347 0         0 for my $i ( 0 .. $#{$S->{oneOf}} )
  0         0  
1348             {
1349 0         0 my $sub_sp = _join_ptr( $sp, 'oneOf', $i );
1350 0         0 my $sub_fn = $self->_compile_js_node( $S->{oneOf}->[ $i ], $sub_sp, $seen, $funcs, $counter_ref, $root, $opts );
1351              
1352 0         0 push( @body, <<JS_RUNTIME );
1353             ctx.errors = [];
1354             // $sub_sp
1355             $sub_fn(inst, path + '/oneOf/$i', ctx);
1356             if(ctx.errors.length === 0)
1357             {
1358             hits++;
1359             }
1360             JS_RUNTIME
1361             }
1362              
1363 0         0 my $sp_qp = _js_quote( $sp );
1364 0         0 push( @body, <<JS_RUNTIME );
1365             ctx.errors = baseErrors;
1366             if(hits !== 1)
1367             {
1368             _jsv_err(ctx, path, 'oneOf', 'exactly one subschema must match, but ' + hits + ' did', $sp_qp);
1369             }
1370             })();
1371             // return;
1372             JS_RUNTIME
1373             }
1374              
1375             # not – but we SKIP "negative required" patterns on the client
1376 0 0       0 if( exists( $S->{not} ) )
1377             {
1378 0         0 my $skip_not = 0;
1379              
1380 0 0       0 if( ref( $S->{not} ) eq 'HASH' )
1381             {
1382 0         0 my $N = $S->{not};
1383              
1384             # Direct: { "not": { "required": [...] } }
1385 0 0 0     0 if( exists( $N->{required} ) &&
    0 0        
1386             ref( $N->{required} ) eq 'ARRAY' )
1387             {
1388 0         0 $skip_not = 1;
1389             }
1390             # Or: { "not": { "anyOf": [ {required:...}, ... ] } }
1391             elsif( exists( $N->{anyOf} ) &&
1392             ref( $N->{anyOf} ) eq 'ARRAY' )
1393             {
1394 0         0 my $all_req = 1;
1395 0         0 for my $elt ( @{$N->{anyOf}} )
  0         0  
1396             {
1397 0 0 0     0 if( !( ref( $elt ) eq 'HASH' &&
      0        
1398             exists( $elt->{required} ) &&
1399             ref( $elt->{required} ) eq 'ARRAY' ) )
1400             {
1401 0         0 $all_req = 0;
1402 0         0 last;
1403             }
1404             }
1405 0 0       0 $skip_not = 1 if( $all_req );
1406             }
1407             }
1408              
1409 0 0       0 if( !$skip_not )
1410             {
1411 0         0 my $sub_sp = _join_ptr( $sp, 'not' );
1412 0         0 my $sub_fn = $self->_compile_js_node( $S->{not}, $sub_sp, $seen, $funcs, $counter_ref, $root, $opts );
1413 0         0 my $sp_qp = _js_quote( $sp );
1414 0         0 push( @body, <<JS_RUNTIME );
1415             (function()
1416             {
1417             var baseErrors = ctx.errors;
1418             ctx.errors = [];
1419             // $sub_sp
1420             $sub_fn(inst, path + '/not', ctx);
1421             var failed = (ctx.errors.length > 0);
1422             ctx.errors = baseErrors;
1423             if(!failed)
1424             {
1425             _jsv_err(ctx, path, 'not', 'instance matched forbidden schema', $sp_qp);
1426             }
1427             })();
1428             // return;
1429             JS_RUNTIME
1430             }
1431             else
1432             {
1433             # Skip "not + required" style rules on the client;
1434             # they are enforced server-side only.
1435 0         0 push( @body, <<JS_RUNTIME );
1436             // NOTE: 'not' at $sp is a negative-required pattern; skipped client-side.
1437             JS_RUNTIME
1438             }
1439             }
1440              
1441             # if / then / else (branch errors are enforced, "if" errors are not)
1442 0 0       0 if( exists( $S->{if} ) )
1443             {
1444 0         0 my $if_sp = _join_ptr( $sp, 'if' );
1445 0         0 my $if_fn = $self->_compile_js_node( $S->{if}, $if_sp, $seen, $funcs, $counter_ref, $root, $opts );
1446              
1447 0         0 push( @body, <<JS_RUNTIME );
1448             (function()
1449             {
1450             var baseErrors = ctx.errors;
1451             var tmp = [];
1452             ctx.errors = tmp;
1453             // $if_sp
1454             $if_fn(inst, path + '/if', ctx);
1455             var failed = (tmp.length > 0);
1456             ctx.errors = baseErrors;
1457             JS_RUNTIME
1458              
1459 0 0       0 if( $S->{then} )
1460             {
1461 0         0 my $then_sp = _join_ptr( $sp, 'then' );
1462 0         0 my $then_fn = $self->_compile_js_node( $S->{then}, $then_sp, $seen, $funcs, $counter_ref, $root, $opts );
1463 0         0 push( @body, <<JS_RUNTIME );
1464             if(!failed)
1465             {
1466             $then_fn(inst, path + '/then', ctx);
1467             }
1468             JS_RUNTIME
1469             }
1470 0 0       0 if( $S->{else} )
1471             {
1472 0         0 my $else_sp = _join_ptr( $sp, 'else' );
1473 0         0 my $else_fn = $self->_compile_js_node( $S->{else}, $else_sp, $seen, $funcs, $counter_ref, $root, $opts );
1474 0         0 push( @body, <<JS_RUNTIME );
1475             else
1476             {
1477             $else_fn(inst, path + '/else', ctx);
1478             }
1479             JS_RUNTIME
1480             }
1481              
1482 0         0 push( @body, <<JS_RUNTIME );
1483             })();
1484             JS_RUNTIME
1485             # Do NOT return here; this node can also have local constraints
1486             }
1487              
1488             # uniqueKeys extension when enabled
1489 0 0 0     0 if( $self->{unique_keys} &&
      0        
1490             exists( $S->{uniqueKeys} ) &&
1491             ref( $S->{uniqueKeys} ) eq 'ARRAY' )
1492             {
1493 0         0 for my $keyset_ref ( @{$S->{uniqueKeys}} )
  0         0  
1494             {
1495 0 0 0     0 next unless( ref( $keyset_ref ) eq 'ARRAY' && @$keyset_ref );
1496              
1497 0         0 my @keys = map{ _js_quote( $_ ) } @$keyset_ref;
  0         0  
1498 0         0 my $qsp = _js_quote( $sp );
1499              
1500 0         0 push( @body, <<JS_RUNTIME );
1501             if(Array.isArray(inst))
1502             {
1503             var seen = {};
1504             for(var i = 0; i < inst.length; i++)
1505             {
1506             var item = inst[i];
1507             var key = '';
1508             try
1509             {
1510             key = [ @keys ].map(function(k){ return item[k]; }).join('\x1E'); // RS separator
1511             }
1512             catch(e) {}
1513             if(seen.hasOwnProperty(key))
1514             {
1515             _jsv_err(ctx, path, 'uniqueKeys', 'duplicate items with keys [@$keyset_ref]', $qsp);
1516             break;
1517             }
1518             seen[key] = true;
1519             }
1520             }
1521             JS_RUNTIME
1522             }
1523             }
1524             }
1525              
1526 0 0 0     0 my $has_type_keyword = ( ref( $S ) eq 'HASH' && exists( $S->{type} ) ) ? 1 : 0;
1527              
1528             # NOTE: type
1529             # 1) type
1530 0 0       0 if( exists( $S->{type} ) )
1531             {
1532 0 0       0 my @types = ref( $S->{type} ) eq 'ARRAY' ? @{$S->{type}} : ( $S->{type} );
  0         0  
1533              
1534             # Special-case "number" vs "integer": _jsv_typeOf returns "integer" for ints
1535 0         0 my @checks;
1536 0         0 for my $t ( @types )
1537             {
1538 0 0       0 if( $t eq 'number' )
1539             {
1540             # accept "number" or "integer" from _jsv_typeOf
1541 0         0 push( @checks, q{tt === 'number' || tt === 'integer'} );
1542             }
1543             else
1544             {
1545 0         0 my $qt = _js_quote( $t );
1546 0         0 push( @checks, "tt === $qt" );
1547             }
1548             }
1549              
1550 0         0 my $cond = join( ' || ', @checks );
1551 0 0       0 my $msg = 'expected type ' .
1552             ( @types == 1 ? $types[0] : '[' . join( ',', @types ) . ']' ) .
1553             ' but found ';
1554 0         0 my $qmsg = _js_quote( $msg );
1555 0         0 my $qsp = _js_quote( $sp );
1556              
1557 0         0 push( @body, <<JS_RUNTIME );
1558             var tt = _jsv_typeOf(inst);
1559             if(!( $cond ))
1560             {
1561             _jsv_err(ctx, path, 'type', $qmsg + tt, $qsp);
1562             return;
1563             }
1564             JS_RUNTIME
1565             }
1566              
1567             # NOTE: enum
1568             # 2) enum
1569 0 0 0     0 if( exists( $S->{enum} ) &&
      0        
1570             ref( $S->{enum} ) eq 'ARRAY' &&
1571 0         0 @{$S->{enum}} )
1572             {
1573 0         0 my @vals = @{$S->{enum}};
  0         0  
1574 0         0 my @vs_js = map{ _js_quote( $_ ) } @vals;
  0         0  
1575 0         0 my $qsp = _js_quote( $sp );
1576              
1577 0         0 local $" = ', ';
1578 0         0 push( @body, <<JS_RUNTIME );
1579             (function()
1580             {
1581             var ok = false;
1582             var v = inst;
1583             var list = [ @vs_js ];
1584             for(var i = 0; i < list.length; i++)
1585             {
1586             if(v === list[i])
1587             {
1588             ok = true;
1589             break;
1590             }
1591             }
1592             if(!ok)
1593             {
1594             _jsv_err(ctx, path, 'enum', 'value is not in enum list', $qsp);
1595             }
1596             })();
1597             JS_RUNTIME
1598             }
1599              
1600             # NOTE: const
1601             # 3) const (primitive only)
1602 0 0       0 if( exists( $S->{const} ) )
1603             {
1604             # For simplicity on the JS side, only support primitive const reliably.
1605             # (Object/array equality is non-trivial; we keep it minimal for now.)
1606 0         0 my $c = $S->{const};
1607 0         0 my $qsp = _js_quote( $sp );
1608              
1609 0 0       0 if( !ref( $c ) )
1610             {
1611 0         0 my $cv = _js_quote( $c );
1612 0         0 push( @body, <<JS_RUNTIME );
1613             if(inst !== $cv)
1614             {
1615             _jsv_err(ctx, path, 'const', 'value does not match const', $qsp);
1616             }
1617             JS_RUNTIME
1618             }
1619             else
1620             {
1621             # Complex const -> skip in JS (server will still enforce)
1622 0         0 push( @body, <<JS_RUNTIME );
1623             // NOTE: const at $sp is non-primitive; not enforced client-side.
1624              
1625             JS_RUNTIME
1626             }
1627             }
1628              
1629             # NOTE: required
1630             # 4) required (objects)
1631 0 0 0     0 if( exists( $S->{required} ) &&
      0        
1632             ref( $S->{required} ) eq 'ARRAY' &&
1633 0         0 @{$S->{required}} )
1634             {
1635 0         0 push( @body, <<JS_RUNTIME );
1636             if(_jsv_typeOf(inst) === 'object')
1637             {
1638             JS_RUNTIME
1639              
1640 0         0 for my $p ( @{$S->{required}} )
  0         0  
1641             {
1642 0         0 my $qp = _js_quote( $p );
1643 0         0 my $sp2 = _join_ptr( $sp, 'properties', $p );
1644 0         0 my $qsp2 = _js_quote( $sp2 );
1645              
1646 0         0 my $msg = "required property '$p' is missing";
1647 0         0 my $qmsg = _js_quote( $msg );
1648              
1649 0         0 push( @body, <<JS_RUNTIME );
1650             if(!_jsv_hasOwn(inst, $qp))
1651             {
1652             _jsv_err(ctx, path, 'required', $qmsg, $qsp2);
1653             }
1654             JS_RUNTIME
1655             }
1656              
1657 0         0 push( @body, <<JS_RUNTIME );
1658             }
1659             JS_RUNTIME
1660             }
1661              
1662             # NOTE: string && pattern
1663             # 5) string length & pattern
1664             my $has_string_constraints =
1665             exists( $S->{minLength} ) ||
1666             exists( $S->{maxLength} ) ||
1667 0   0     0 exists( $S->{pattern} );
1668              
1669 0 0       0 if( $has_string_constraints )
1670             {
1671 0         0 my $qsp = _js_quote( $sp );
1672              
1673 0         0 push( @body, <<JS_RUNTIME );
1674             if(_jsv_typeOf(inst) === 'string')
1675             {
1676             JS_RUNTIME
1677              
1678 0 0       0 if( exists( $S->{minLength} ) )
1679             {
1680 0         0 my $min = int( $S->{minLength} );
1681 0         0 push( @body, <<JS_RUNTIME );
1682             if(inst.length < $min)
1683             {
1684             _jsv_err(ctx, path, 'minLength', 'string shorter than minLength $min', $qsp);
1685             }
1686             JS_RUNTIME
1687             }
1688              
1689 0 0       0 if( exists( $S->{maxLength} ) )
1690             {
1691 0         0 my $max = int( $S->{maxLength} );
1692 0         0 push( @body, <<JS_RUNTIME );
1693             if(inst.length > $max)
1694             {
1695             _jsv_err(ctx, path, 'maxLength', 'string longer than maxLength $max', $qsp);
1696             }
1697             JS_RUNTIME
1698             }
1699              
1700 0 0 0     0 if( exists( $S->{pattern} ) &&
      0        
1701             defined( $S->{pattern} ) &&
1702             length( $S->{pattern} ) )
1703             {
1704 0         0 my $pat = $S->{pattern};
1705             # my $qpat = _js_quote( $pat );
1706             # from \x{FF70} to \uFF70
1707             # from \p{Katakana} to \p{sc=Katakana}
1708 0         0 my $qpat = _re_to_js( $pat );
1709              
1710 0 0       0 if( $force_unicode )
1711             {
1712             # ecma >= 2018: always try with "u" flag (Unicode mode)
1713 0         0 push( @body, <<JS_RUNTIME );
1714             try
1715             {
1716             var re = new RegExp("$qpat", "u");
1717             if(!re.test(inst))
1718             {
1719             _jsv_err(ctx, path, 'pattern', 'string does not match pattern', $qsp);
1720             }
1721             }
1722             catch(e)
1723             {
1724             // Browser does not support Unicode property escapes or this pattern.
1725             }
1726             JS_RUNTIME
1727             }
1728             else
1729             {
1730             # auto / ES5 mode: detect "advanced" patterns
1731 0 0       0 if( $pat =~ /\\p\{|\\P\{|\\X|\\R|\(\?[A-Za-z]/ )
1732             {
1733             # Attempt Unicode mode with "u" flag, but gracefully fall back
1734 0         0 push( @body, <<JS_RUNTIME );
1735             (function()
1736             {
1737             var re = null;
1738             try
1739             {
1740             re = new RegExp("$qpat", "u");
1741             }
1742             catch(e)
1743             {
1744             // Older browser; skip Unicode-property-based pattern on client.
1745             }
1746             if(re && !re.test(inst))
1747             {
1748             _jsv_err(ctx, path, 'pattern', 'string does not match pattern', $qsp);
1749             }
1750             })();
1751             JS_RUNTIME
1752             }
1753             else
1754             {
1755             # Simple ECMA 5-compatible pattern
1756 0         0 push( @body, <<JS_RUNTIME );
1757             try
1758             {
1759             var re = new RegExp("$qpat");
1760             if(!re.test(inst))
1761             {
1762             _jsv_err(ctx, path, 'pattern', 'string does not match pattern', $qsp);
1763             }
1764             }
1765             catch(e)
1766             {
1767             // If pattern is not JS-compatible, we silently skip on the client.
1768             }
1769             JS_RUNTIME
1770             }
1771             }
1772             }
1773              
1774 0         0 push( @body, <<JS_RUNTIME );
1775             }
1776             JS_RUNTIME
1777             }
1778              
1779             # NOTE: minimum, maximum, etc
1780             # 6) numeric bounds (minimum/maximum/exclusive*)
1781             my $has_num_constraints =
1782             exists( $S->{minimum} ) ||
1783             exists( $S->{maximum} ) ||
1784             exists( $S->{exclusiveMinimum} ) ||
1785 0   0     0 exists( $S->{exclusiveMaximum} );
1786              
1787 0 0       0 if( $has_num_constraints )
1788             {
1789             # For consistency with string/pattern/etc, we report the
1790             # schema pointer of the *owning schema* ($sp), not the
1791             # child keyword location (.../minimum).
1792 0         0 my $qsp_num = _js_quote( $sp );
1793 0         0 my $qmsg_expected_num = _js_quote( 'expected number but found ' );
1794              
1795 0         0 push( @body, <<'JS_RUNTIME' );
1796             var t = _jsv_typeOf(inst);
1797              
1798             // Coerce numeric-looking strings to numbers, for friendlier UX
1799             if(t === 'string' && /^[+-]?(?:\d+|\d*\.\d+)$/.test(inst))
1800             {
1801             inst = +inst;
1802             t = _jsv_typeOf(inst);
1803             }
1804              
1805             if(t === 'number' || t === 'integer')
1806             {
1807             JS_RUNTIME
1808              
1809 0 0       0 if( exists( $S->{minimum} ) )
1810             {
1811 0         0 my $min = 0 + $S->{minimum};
1812 0         0 push( @body, <<JS_RUNTIME );
1813             if(inst < $min)
1814             {
1815             _jsv_err(ctx, path, 'minimum', 'number is less than minimum $min', $qsp_num);
1816             }
1817             JS_RUNTIME
1818             }
1819              
1820 0 0       0 if( exists( $S->{maximum} ) )
1821             {
1822 0         0 my $max = 0 + $S->{maximum};
1823 0         0 push( @body, <<JS_RUNTIME );
1824             if(inst > $max)
1825             {
1826             _jsv_err(ctx, path, 'maximum', 'number is greater than maximum $max', $qsp_num);
1827             }
1828             JS_RUNTIME
1829             }
1830              
1831 0 0       0 if( exists( $S->{exclusiveMinimum} ) )
1832             {
1833 0         0 my $emin = 0 + $S->{exclusiveMinimum};
1834 0         0 push( @body, <<JS_RUNTIME );
1835             if(inst <= $emin)
1836             {
1837             _jsv_err(ctx, path, 'exclusiveMinimum',
1838             'number is <= exclusiveMinimum $emin', $qsp_num);
1839             }
1840             JS_RUNTIME
1841             }
1842              
1843 0 0       0 if( exists( $S->{exclusiveMaximum} ) )
1844             {
1845 0         0 my $emax = 0 + $S->{exclusiveMaximum};
1846 0         0 push( @body, <<JS_RUNTIME );
1847             if(inst >= $emax)
1848             {
1849             _jsv_err(ctx, path, 'exclusiveMaximum',
1850             'number is >= exclusiveMaximum $emax', $qsp_num);
1851             }
1852             JS_RUNTIME
1853             }
1854              
1855             # Close the "if(t === 'number' || t === 'integer')" and, if needed,
1856             # add a fallback type error when no explicit "type" keyword exists.
1857 0 0       0 if( $has_type_keyword )
1858             {
1859 0         0 push( @body, <<'JS_RUNTIME' );
1860             }
1861             JS_RUNTIME
1862             }
1863             else
1864             {
1865 0         0 push( @body, <<JS_RUNTIME );
1866             }
1867             else
1868             {
1869             _jsv_err(ctx, path, 'type', $qmsg_expected_num + t, $qsp_num);
1870             }
1871             JS_RUNTIME
1872             }
1873             }
1874              
1875             # NOTE: items / minItems / maxItems
1876             # 7) array items & minItems/maxItems
1877             my $has_array_len =
1878             exists( $S->{minItems} ) ||
1879 0   0     0 exists( $S->{maxItems} );
1880              
1881 0 0 0     0 if( exists( $S->{items} ) || $has_array_len )
1882             {
1883 0         0 my $qsp = _js_quote( $sp );
1884              
1885             # Precompile single-schema "items"
1886 0         0 my $items_fn;
1887 0 0 0     0 if( exists( $S->{items} ) && ref( $S->{items} ) eq 'HASH' )
1888             {
1889 0         0 my $items_ptr = _join_ptr( $sp, 'items' );
1890 0         0 $items_fn = $self->_compile_js_node( $S->{items}, $items_ptr, $seen, $funcs, $counter_ref, $root, $opts );
1891             }
1892              
1893 0 0       0 my $min_items = exists( $S->{minItems} ) ? int( $S->{minItems} ) : undef;
1894 0 0       0 my $max_items = exists( $S->{maxItems} ) ? int( $S->{maxItems} ) : undef;
1895              
1896 0         0 my( $qmsg_min, $qmsg_max );
1897 0 0       0 if( defined( $min_items ) )
1898             {
1899 0         0 my $msg_min = "array has fewer than $min_items items";
1900 0         0 $qmsg_min = _js_quote( $msg_min );
1901             }
1902 0 0       0 if( defined( $max_items ) )
1903             {
1904 0         0 my $msg_max = "array has more than $max_items items";
1905 0         0 $qmsg_max = _js_quote( $msg_max );
1906             }
1907              
1908 0         0 push( @body, <<JS_RUNTIME );
1909             if(_jsv_typeOf(inst) === 'array')
1910             {
1911             JS_RUNTIME
1912              
1913 0 0       0 if( defined( $min_items ) )
1914             {
1915 0         0 push( @body, <<JS_RUNTIME );
1916             if(inst.length < $min_items)
1917             {
1918             _jsv_err(ctx, path, 'minItems', $qmsg_min, $qsp);
1919             }
1920             JS_RUNTIME
1921             }
1922              
1923 0 0       0 if( defined( $max_items ) )
1924             {
1925 0         0 push( @body, <<JS_RUNTIME );
1926             if(inst.length > $max_items)
1927             {
1928             _jsv_err(ctx, path, 'maxItems', $qmsg_max, $qsp);
1929             }
1930             JS_RUNTIME
1931             }
1932              
1933 0 0       0 if( $items_fn )
1934             {
1935 0         0 push( @body, <<JS_RUNTIME );
1936             for(var i = 0; i < inst.length; i++)
1937             {
1938             $items_fn(inst[i], path + '/' + i, ctx);
1939             if(ctx.maxErrors && ctx.errors && ctx.errors.length >= ctx.maxErrors)
1940             {
1941             return;
1942             }
1943             }
1944             JS_RUNTIME
1945             }
1946              
1947 0         0 push( @body, <<JS_RUNTIME );
1948             }
1949             JS_RUNTIME
1950             }
1951              
1952             # NOTE: properties
1953             # 8) properties (objects) – recurse into children
1954 0 0 0     0 if( exists( $S->{properties} ) &&
1955             ref( $S->{properties} ) eq 'HASH' )
1956             {
1957 0         0 push( @body, <<JS_RUNTIME );
1958             if(_jsv_typeOf(inst) === 'object')
1959             {
1960             JS_RUNTIME
1961              
1962 0         0 for my $p ( sort( keys( %{$S->{properties}} ) ) )
  0         0  
1963             {
1964 0         0 my $child = $S->{properties}->{ $p };
1965 0         0 my $child_ptr = _join_ptr( $sp, 'properties', $p );
1966 0         0 my $child_fn = $self->_compile_js_node( $child, $child_ptr, $seen, $funcs, $counter_ref, $root, $opts );
1967              
1968 0         0 my $qp = _js_quote( $p );
1969 0         0 my $path_suffix = '/' . $p;
1970 0         0 my $path_suffix_q = _js_quote( $path_suffix );
1971              
1972 0         0 push( @body, <<JS_RUNTIME );
1973             if(_jsv_hasOwn(inst, $qp))
1974             {
1975             // $child_ptr
1976             $child_fn(inst[$qp], path + $path_suffix_q, ctx);
1977             }
1978             JS_RUNTIME
1979             }
1980              
1981 0         0 push( @body, <<JS_RUNTIME );
1982             }
1983             JS_RUNTIME
1984             }
1985              
1986             # NOTE: definitions – recurse into named schemas
1987 0 0 0     0 if( exists( $S->{definitions} ) &&
1988             ref( $S->{definitions} ) eq 'HASH' )
1989             {
1990 0         0 for my $name ( sort keys %{ $S->{definitions} } )
  0         0  
1991             {
1992 0         0 my $child = $S->{definitions}->{ $name };
1993 0         0 my $child_ptr = _join_ptr( $sp, 'definitions', $name );
1994 0         0 my $child_fn = $self->_compile_js_node( $child, $child_ptr, $seen, $funcs, $counter_ref, $root, $opts );
1995              
1996             # No runtime call needed here — definitions don't validate by themselves.
1997             # We just need them compiled so pointer-based lookup can see them.
1998             }
1999             }
2000              
2001             # NOTE: contains
2002             # 9) contains / minContains / maxContains (arrays)
2003 0 0       0 if( exists( $S->{contains} ) )
2004             {
2005 0         0 my $contains_schema = $S->{contains};
2006 0         0 my $contains_ptr = _join_ptr( $sp, 'contains' );
2007 0         0 my $contains_fn = $self->_compile_js_node( $contains_schema, $contains_ptr, $seen, $funcs, $counter_ref, $root, $opts );
2008 0         0 my $qsp_contains = _js_quote( $contains_ptr );
2009              
2010 0         0 my $have_min = exists( $S->{minContains} );
2011 0         0 my $have_max = exists( $S->{maxContains} );
2012 0 0       0 my $min = $have_min ? int( $S->{minContains} ) : 0;
2013 0 0       0 my $max = $have_max ? int( $S->{maxContains} ) : 0;
2014              
2015 0         0 my $msg_min = "array has fewer than $min items matching contains subschema";
2016 0         0 my $qmsg_min = _js_quote( $msg_min );
2017 0         0 my $msg_max = "array has more than $max items matching contains subschema";
2018 0         0 my $qmsg_max = _js_quote( $msg_max );
2019 0         0 my $msg_cont = "array does not contain any item matching contains subschema";
2020 0         0 my $qmsg_cont= _js_quote( $msg_cont );
2021              
2022 0         0 push( @body, <<JS_RUNTIME );
2023             if(_jsv_typeOf(inst) === 'array')
2024             {
2025             var matchCount = 0;
2026             for(var i = 0; i < inst.length; i++)
2027             {
2028             var tmpCtx = { errors: [], maxErrors: ctx.maxErrors };
2029             // $contains_ptr
2030             $contains_fn(inst[i], path + '/' + i, tmpCtx);
2031             if(tmpCtx.errors.length === 0)
2032             {
2033             matchCount++;
2034             }
2035             }
2036             JS_RUNTIME
2037              
2038 0 0       0 if( $have_min )
2039             {
2040 0         0 push( @body, <<JS_RUNTIME );
2041             if(matchCount < $min)
2042             {
2043             _jsv_err(ctx, path, 'minContains', $qmsg_min, $qsp_contains);
2044             }
2045             JS_RUNTIME
2046             }
2047              
2048 0 0       0 if( $have_max )
2049             {
2050 0         0 push( @body, <<JS_RUNTIME );
2051             if(matchCount > $max)
2052             {
2053             _jsv_err(ctx, path, 'maxContains', $qmsg_max, $qsp_contains);
2054             }
2055             JS_RUNTIME
2056             }
2057              
2058             # Plain "contains" only if no min/max are present
2059 0 0 0     0 if( !$have_min && !$have_max )
2060             {
2061 0         0 push( @body, <<JS_RUNTIME );
2062             if(matchCount === 0)
2063             {
2064             _jsv_err(ctx, path, 'contains', $qmsg_cont, $qsp_contains);
2065             }
2066             JS_RUNTIME
2067             }
2068              
2069 0         0 push( @body, <<JS_RUNTIME );
2070             }
2071             JS_RUNTIME
2072             }
2073              
2074 0         0 push( @body, <<JS_RUNTIME );
2075             }
2076             JS_RUNTIME
2077              
2078 0         0 push( @$funcs, join( '', @body ) );
2079              
2080 0         0 return( $fn );
2081             }
2082              
2083             sub _compile_node
2084             {
2085 38     38   90 my( $root, $ptr, $S ) = @_;
2086              
2087             # Non-hash schemas (incl. booleans) => trivial pass
2088             return sub
2089             {
2090 0     0   0 my( $ctx, $inst ) = @_;
2091 0         0 return( { ok => 1, props => {}, items => {} } );
2092 38 50       90 } unless( ref( $S ) eq 'HASH' );
2093              
2094             # Capture presence and values so runtime avoids hash lookups
2095 38         92 my $has_type = exists( $S->{type} );
2096 38         65 my $type_spec = $S->{type};
2097              
2098 38         59 my $has_const = exists( $S->{const} );
2099 38         83 my $const_val = $S->{const};
2100              
2101 38         68 my $has_enum = exists( $S->{enum} );
2102 38         60 my $enum_vals = $S->{enum};
2103              
2104 38         66 my %numk = map{ $_ => $S->{ $_ } } grep{ exists( $S->{ $_ } ) }
  2         9  
  190         353  
2105             qw( multipleOf minimum maximum exclusiveMinimum exclusiveMaximum );
2106              
2107 38 100 33     227 my $has_strlen = ( exists( $S->{minLength} ) || exists( $S->{maxLength} ) || exists( $S->{pattern} ) ) ? 1 : 0;
2108 38         57 my $has_format = exists( $S->{format} );
2109 38         49 my $format_name = $S->{format};
2110              
2111             my $has_unique_keys =
2112             exists( $S->{uniqueKeys} ) &&
2113 38   33     89 ref( $S->{uniqueKeys} ) eq 'ARRAY';
2114              
2115             # Precompile child closures (same structure our interpreter walks)
2116 38         48 my %child;
2117              
2118             # Arrays
2119 38 50       82 if( ref( $S->{prefixItems} ) eq 'ARRAY' )
2120             {
2121 0         0 for my $i ( 0 .. $#{ $S->{prefixItems} } )
  0         0  
2122             {
2123 0         0 my $cp = _join_ptr( $ptr, "prefixItems/$i" );
2124 0         0 $child{ "prefix:$i" } = _compile_node( $root, $cp, $S->{prefixItems}->[$i] );
2125             }
2126             }
2127 38 50       88 if( ref( $S->{items} ) eq 'HASH' )
2128             {
2129 0         0 $child{ "items" } = _compile_node( $root, _join_ptr( $ptr, "items" ), $S->{items} );
2130             }
2131 38 50 33     104 if( exists( $S->{contains} ) && ref( $S->{contains} ) )
2132             {
2133 0         0 $child{ "contains" } = _compile_node( $root, _join_ptr( $ptr, "contains" ), $S->{contains} );
2134             }
2135 38 50 33     110 if( exists( $S->{unevaluatedItems} ) && ref( $S->{unevaluatedItems} ) eq 'HASH' )
2136             {
2137 0         0 $child{ "unevaluatedItems" } = _compile_node( $root, _join_ptr( $ptr, "unevaluatedItems" ), $S->{unevaluatedItems} );
2138             }
2139              
2140             # Objects
2141 38 100       90 if( ref( $S->{properties} ) eq 'HASH' )
2142             {
2143 11         18 for my $k ( keys( %{$S->{properties}} ) )
  11         38  
2144             {
2145 15         56 my $cp = _join_ptr( $ptr, "properties/$k" );
2146 15         114 $child{ "prop:$k" } = _compile_node( $root, $cp, $S->{properties}->{ $k } );
2147             }
2148             }
2149 38 50       87 if( ref( $S->{patternProperties} ) eq 'HASH' )
2150             {
2151 0         0 for my $re ( keys( %{ $S->{patternProperties} } ) )
  0         0  
2152             {
2153 0         0 my $cp = _join_ptr( $ptr, "patternProperties/$re" );
2154 0         0 $child{ "pat:$re" } = _compile_node( $root, $cp, $S->{patternProperties}->{ $re } );
2155             }
2156             }
2157 38 50 66     120 if( exists( $S->{additionalProperties} ) && ref( $S->{additionalProperties} ) eq 'HASH' )
2158             {
2159 0         0 $child{ "additional" } = _compile_node( $root, _join_ptr( $ptr, "additionalProperties" ), $S->{additionalProperties} );
2160             }
2161 38 50 33     88 if( exists( $S->{propertyNames} ) && ref( $S->{propertyNames} ) eq 'HASH' )
2162             {
2163 0         0 $child{ "propnames" } = _compile_node( $root, _join_ptr( $ptr, "propertyNames" ), $S->{propertyNames} );
2164             }
2165 38 50 33     90 if( exists( $S->{dependentSchemas} ) && ref( $S->{dependentSchemas} ) eq 'HASH' )
2166             {
2167 0         0 for my $k ( keys( %{$S->{dependentSchemas}} ) )
  0         0  
2168             {
2169 0         0 my $cp = _join_ptr( $ptr, "dependentSchemas/$k" );
2170 0         0 $child{ "deps:$k" } = _compile_node( $root, $cp, $S->{dependentSchemas}->{ $k } );
2171             }
2172             }
2173 38 50 33     106 if( exists( $S->{unevaluatedProperties} ) && ref( $S->{unevaluatedProperties} ) eq 'HASH' )
2174             {
2175 0         0 $child{ "ueprops" } = _compile_node( $root, _join_ptr( $ptr, "unevaluatedProperties" ), $S->{unevaluatedProperties} );
2176             }
2177              
2178             # Combinators
2179 38         71 for my $kw ( qw( allOf anyOf oneOf not ) )
2180             {
2181 152 50       270 next unless( exists( $S->{ $kw } ) );
2182 0 0       0 if( $kw eq 'not' )
2183             {
2184 0 0       0 $child{ "not" } = _compile_node( $root, _join_ptr( $ptr, "not" ), $S->{not} ) if( ref( $S->{not} ) );
2185 0         0 next;
2186             }
2187 0 0       0 if( ref( $S->{ $kw } ) eq 'ARRAY' )
2188             {
2189 0         0 for my $i ( 0 .. $#{$S->{ $kw }} )
  0         0  
2190             {
2191 0         0 my $cp = _join_ptr( $ptr, "$kw/$i" );
2192 0         0 $child{ "$kw:$i" } = _compile_node( $root, $cp, $S->{ $kw }->[$i] );
2193             }
2194             }
2195             }
2196              
2197             # Conditionals
2198 38 50 33     87 if( exists( $S->{if} ) && ref( $S->{if} ) eq 'HASH' )
2199             {
2200 0         0 $child{ "if" } = _compile_node( $root, _join_ptr( $ptr, 'if' ), $S->{if} );
2201 0 0 0     0 $child{ "then" } = _compile_node( $root, _join_ptr( $ptr, 'then' ), $S->{then} ) if( exists( $S->{then} ) && ref( $S->{then} ) );
2202 0 0 0     0 $child{ "else" } = _compile_node( $root, _join_ptr( $ptr, 'else' ), $S->{else} ) if( exists( $S->{else} ) && ref( $S->{else} ) );
2203             }
2204              
2205             # Return specialized validator
2206             return sub
2207             {
2208 35     35   64 my( $ctx, $inst ) = @_;
2209              
2210             # Parity with interpreter: trace node visit
2211 35 100       113 _t( $ctx, $ptr, 'node', undef, 'visit' ) if( $ctx->{trace_on} );
2212              
2213             # Type / const / enum
2214 35 50       63 if( $has_type ) { _k_type( $ctx, $inst, $type_spec, $ptr ) or return( _fail() ); }
  35 50       93  
2215 35 0       76 if( $has_const ) { _k_const( $ctx, $inst, $const_val, $ptr ) or return( _fail() ); }
  0 50       0  
2216 35 0       76 if( $has_enum ) { _k_enum( $ctx, $inst, $enum_vals, $ptr ) or return( _fail() ); }
  0 50       0  
2217              
2218             # uniqueKeys extension (compiled path)
2219 35 0 33     84 if( $ctx->{unique_keys} && $has_unique_keys && ref( $inst ) eq 'ARRAY' )
      33        
2220             {
2221 0         0 my $r = _k_unique_keys( $ctx, $ptr, $S->{uniqueKeys}, $inst );
2222 0 0       0 return( $r ) unless( $r->{ok} );
2223             }
2224              
2225             # Numbers
2226 35 100       78 if( _is_number( $inst ) )
2227             {
2228 6         14 for my $k ( qw( multipleOf minimum maximum exclusiveMinimum exclusiveMaximum ) )
2229             {
2230 27 100       67 next unless( exists( $numk{ $k } ) );
2231 2 100       8 _k_number( $ctx, $inst, $k, $numk{$k}, $ptr ) or return( _fail() );
2232             }
2233             }
2234              
2235             # Strings
2236 34 100 66     105 if( !ref( $inst ) && defined( $inst ) )
2237             {
2238 17 100       32 if( $has_strlen ) { _k_string( $ctx, $inst, $S, $ptr ) or return( _fail() ); }
  2 100       7  
2239 16 100       49 if( $has_format ) { _k_format( $ctx, $inst, $format_name, $ptr ) or return( _fail() ); }
  2 100       9  
2240              
2241             # contentEncoding / contentMediaType / contentSchema (compiled path)
2242 15 50 33     151 if( exists( $S->{contentEncoding} ) ||
      33        
2243             exists( $S->{contentMediaType} ) ||
2244             exists( $S->{contentSchema} ) )
2245             {
2246 0 0       0 my $assert = $ctx->{content_assert} ? 1 : 0;
2247 0         0 my $bytes = "$inst";
2248 0         0 my $decoded_ref;
2249              
2250 0 0       0 if( exists( $S->{contentEncoding} ) )
2251             {
2252 0         0 my $dec = _content_decode( $ctx, $S->{contentEncoding}, $bytes );
2253 0 0       0 if( !defined( $dec ) )
2254             {
2255 0 0       0 return( _err_res( $ctx, $ptr, "contentEncoding '$S->{contentEncoding}' decode failed", 'contentEncoding' ) ) if( $assert );
2256             }
2257             else
2258             {
2259 0         0 $bytes = $dec;
2260             }
2261             }
2262              
2263 0 0       0 if( exists( $S->{contentMediaType} ) )
2264             {
2265 0         0 my( $mt, $params ) = _parse_media_type( $S->{contentMediaType} );
2266 0 0       0 if( my $cb = $ctx->{media_validators}->{ $mt } )
2267             {
2268 0         0 my( $ok, $msg, $maybe_decoded ) = _safe_invoke( $cb, $bytes, $params );
2269 0 0       0 if( !$ok )
2270             {
2271 0 0 0     0 return( _err_res( $ctx, $ptr, ( $msg || "contentMediaType '$mt' validation failed" ), 'contentMediaType' ) ) if( $assert );
2272             }
2273             # If the media validator decoded into a Perl structure, keep it
2274 0 0       0 $decoded_ref = $maybe_decoded if( ref( $maybe_decoded ) );
2275             # If it produced new octets/text, keep that too
2276 0 0 0     0 $bytes = $maybe_decoded if( defined( $maybe_decoded ) && !ref( $maybe_decoded ) );
2277             }
2278             else
2279             {
2280 0 0 0     0 if( $mt =~ m{\Atext/} && ( ( $params->{charset} || '' ) =~ /\Autf-?8\z/i ) )
      0        
2281             {
2282 0         0 local $@;
2283             my $ok = eval
2284 0 0       0 {
2285 0         0 require Encode;
2286 0         0 Encode::decode( 'UTF-8', $bytes, Encode::FB_CROAK );
2287 0         0 1;
2288             } ? 1 : 0;
2289 0 0 0     0 if( !$ok && $assert )
2290             {
2291 0         0 return( _err_res( $ctx, $ptr, "contentMediaType '$mt' invalid UTF-8", 'contentMediaType' ) );
2292             }
2293             }
2294             }
2295             }
2296              
2297 0 0       0 if( exists( $S->{contentSchema} ) )
2298             {
2299 0         0 my $val;
2300              
2301 0 0       0 if( ref( $decoded_ref ) )
2302             {
2303             # already decoded by media validator (e.g. application/json)
2304 0         0 $val = $decoded_ref;
2305             }
2306             else
2307             {
2308 0         0 local $@;
2309             # still a string of bytes; try JSON decode now
2310 0         0 $val = eval{ JSON->new->allow_nonref(1)->utf8(1)->decode( $bytes ) };
  0         0  
2311             }
2312              
2313 0 0       0 if( !defined( $val ) )
2314             {
2315 0 0       0 return( _err_res( $ctx, $ptr, "contentSchema present but payload not JSON-decodable", 'contentSchema' ) ) if( $assert );
2316             }
2317             else
2318             {
2319 0         0 my $r = _v( $ctx, _join_ptr( $ptr, 'contentSchema' ), $S->{contentSchema}, $val );
2320 0 0       0 return( $r ) unless( $r->{ok} );
2321             }
2322             }
2323             }
2324             }
2325              
2326 32         96 my %ann_props;
2327             my %ann_items;
2328              
2329             # Arrays
2330 32 50       67 if( ref( $inst ) eq 'ARRAY' )
2331             {
2332 0         0 my $r = _k_array_all( $ctx, $ptr, $S, $inst );
2333 0 0       0 return( $r ) unless( $r->{ok} );
2334 0         0 %ann_items = ( %ann_items, %{$r->{items}} );
  0         0  
2335             }
2336              
2337             # Objects
2338 32 100       64 if( ref( $inst ) eq 'HASH' )
2339             {
2340 17         51 my $r = _k_object_all( $ctx, $ptr, $S, $inst );
2341 17 100       98 return( $r ) unless( $r->{ok} );
2342 10         21 %ann_props = ( %ann_props, %{$r->{props}} );
  10         38  
2343             }
2344              
2345             # Combinators
2346             # allOf: all subschemas must pass, and we merge their annotations.
2347 25 50 33     66 if( exists( $S->{allOf} ) && ref( $S->{allOf} ) eq 'ARRAY' )
2348             {
2349 0         0 my( %p, %it );
2350 0         0 for my $i ( 0 .. $#{ $S->{allOf} } )
  0         0  
2351             {
2352 0         0 my $r = $child{ "allOf:$i" }->( $ctx, $inst );
2353 0 0       0 return $r unless $r->{ok};
2354 0         0 %p = ( %p, %{$r->{props}} );
  0         0  
2355 0         0 %it = ( %it, %{$r->{items}} );
  0         0  
2356             }
2357 0         0 %ann_props = ( %ann_props, %p );
2358 0         0 %ann_items = ( %ann_items, %it );
2359             }
2360              
2361             # anyOf: at least one subschema must pass; do NOT leak errors from
2362             # non-selected branches into the main context.
2363 25 50 33     66 if( exists( $S->{anyOf} ) && ref( $S->{anyOf} ) eq 'ARRAY' )
2364             {
2365 0         0 my $ok = 0;
2366 0         0 my( %p, %it );
2367 0         0 my @branch_errs;
2368              
2369 0         0 for my $i ( 0 .. $#{ $S->{anyOf} } )
  0         0  
2370             {
2371             # Shadow context for this branch
2372 0         0 my %shadow = %$ctx;
2373 0         0 my @errs;
2374 0         0 $shadow{errors} = \@errs;
2375 0         0 $shadow{error_count} = 0;
2376              
2377 0         0 my $r = $child{ "anyOf:$i" }->( \%shadow, $inst );
2378              
2379 0 0       0 if( $r->{ok} )
2380             {
2381 0         0 $ok = 1;
2382 0         0 %p = ( %p, %{$r->{props}} );
  0         0  
2383 0         0 %it = ( %it, %{$r->{items}} );
  0         0  
2384 0         0 last;
2385             }
2386              
2387 0         0 push( @branch_errs, \@errs );
2388             }
2389              
2390 0 0       0 unless( $ok )
2391             {
2392             # No branch matched: merge collected branch errors into main context
2393 0         0 for my $aref ( @branch_errs )
2394             {
2395 0 0       0 next unless( @$aref );
2396 0         0 push( @{$ctx->{errors}}, @$aref );
  0         0  
2397 0         0 $ctx->{error_count} += scalar( @$aref );
2398             }
2399              
2400 0         0 return( _err_res( $ctx, $ptr, "instance does not satisfy anyOf", 'anyOf' ) );
2401             }
2402              
2403 0         0 %ann_props = ( %ann_props, %p );
2404 0         0 %ann_items = ( %ann_items, %it );
2405             }
2406              
2407             # oneOf: exactly one subschema must pass; again, isolate branch errors.
2408 25 50 33     123 if( exists( $S->{oneOf} ) && ref( $S->{oneOf} ) eq 'ARRAY' )
2409             {
2410 0         0 my $hits = 0;
2411              
2412 0         0 for my $i ( 0 .. $#{ $S->{oneOf} } )
  0         0  
2413             {
2414 0         0 my %shadow = %$ctx;
2415 0         0 my @errs;
2416 0         0 $shadow{errors} = \@errs;
2417 0         0 $shadow{error_count} = 0;
2418              
2419 0         0 my $r = $child{ "oneOf:$i" }->( \%shadow, $inst );
2420 0 0       0 $hits++ if( $r->{ok} );
2421             }
2422              
2423 0 0       0 return( _err_res( $ctx, $ptr, "instance satisfies $hits subschemas in oneOf (expected exactly 1)", 'oneOf' ) )
2424             unless( $hits == 1 );
2425             }
2426              
2427             # not: the inner schema must **fail**; its own errors are irrelevant
2428             # on success, so we run it entirely in a shadow context.
2429 25 50 33     54 if( exists( $S->{not} ) && ref( $S->{not} ) )
2430             {
2431 0         0 my %shadow = %$ctx;
2432 0         0 my @errs;
2433 0         0 $shadow{errors} = \@errs;
2434 0         0 $shadow{error_count} = 0;
2435              
2436 0         0 my $r = $child{ "not" }->( \%shadow, $inst );
2437              
2438             # If inner schema passes, then "not" fails
2439             return( _err_res( $ctx, $ptr, "instance matches forbidden not-schema", 'not' ) )
2440 0 0       0 if( $r->{ok} );
2441              
2442             # Otherwise, "not" is satisfied; ignore inner errors entirely
2443             }
2444              
2445             # Conditionals
2446 25 50 33     70 if( exists( $S->{if} ) && ref( $S->{if} ) )
2447             {
2448 0         0 my $cond = $child{ "if" }->( $ctx, $inst );
2449 0 0       0 if( $cond->{ok} )
2450             {
2451 0 0       0 if( exists( $child{ "then" } ) )
2452             {
2453 0         0 my $r = $child{ "then" }->( $ctx, $inst );
2454 0 0       0 return( $r ) unless( $r->{ok} );
2455             }
2456             }
2457             else
2458             {
2459 0 0       0 if( exists( $child{ "else" } ) )
2460             {
2461 0         0 my $r = $child{ "else" }->( $ctx, $inst );
2462 0 0       0 return( $r ) unless( $r->{ok} );
2463             }
2464             }
2465             }
2466              
2467 25 100       73 _t( $ctx, $ptr, 'node', undef, 'pass' ) if( $ctx->{trace_on} );
2468 25         125 return( { ok => 1, props => \%ann_props, items => \%ann_items } );
2469 38         905 };
2470             }
2471              
2472             # Compilation / Indexing
2473             sub _compile_root
2474             {
2475 114     114   313 my( $schema ) = @_;
2476              
2477 114 50       463 if( ref( $schema ) eq 'HASH' )
2478             {
2479 114 100 33     571 $schema->{'$defs'} ||= delete( $schema->{definitions} ) if( exists( $schema->{definitions} ) );
2480             }
2481              
2482 114 100 66     1034 my $base = _normalize_uri( ( ref( $schema ) eq 'HASH' && $schema->{'$id'} ) ? $schema->{'$id'} : '#' );
2483              
2484 114         288 my $anchors = {};
2485 114         202 my $id_index = {};
2486              
2487 114         500 _index_schema_202012( $schema, $base, '#', $anchors, $id_index );
2488              
2489             return({
2490 114         908 schema => $schema,
2491             anchors => $anchors, # "#/a/b/0"
2492             id_index => $id_index, # absolute IDs and #anchors
2493             base => $base,
2494             fn_index => {}, # cache of ptr => compiled closure
2495             });
2496             }
2497              
2498             sub _content_decode
2499             {
2500 9     9   22 my( $ctx, $enc, $s ) = @_;
2501 9   50     26 $enc = lc( $enc // '' );
2502              
2503 9 100       30 if( my $cb = $ctx->{content_decoders}->{ $enc } )
2504             {
2505 3         10 my( $ok, $msg, $out ) = _safe_invoke( $cb, $s );
2506 3 50       14 return( $ok ? $out : undef );
2507             }
2508              
2509 6 50       15 if( $enc eq 'base64' )
2510             {
2511 6         36 my $out = _strict_base64_decode( $s );
2512             # undef on failure is exactly what we want
2513 6         15 return( $out );
2514             }
2515              
2516             # Unknown encoding (annotation only unless assert)
2517 0         0 return;
2518             }
2519              
2520             # Errors, utils, pointers, URIs
2521             sub _err
2522             {
2523 176     176   389 my( $ctx, $schema_ptr, $msg, $keyword ) = @_;
2524 176 50       612 return(0) if( $ctx->{error_count} >= $ctx->{max_errors} );
2525              
2526             # Instance path: use current ptr_stack top if available, else '#'
2527             my $inst_path = $ctx->{ptr_stack} && @{$ctx->{ptr_stack}}
2528 176 50 33     540 ? ( $ctx->{ptr_stack}->[-1] // '#' )
      50        
2529             : '#';
2530              
2531 176         345 push( @{$ctx->{errors}}, JSON::Schema::Validate::Error->new(
  176         1059  
2532             path => $inst_path,
2533             message => $msg,
2534             keyword => $keyword, # may be undef (back-compat)
2535             schema_pointer => $schema_ptr, # where in the schema this came from
2536             ));
2537 176         309 $ctx->{error_count}++;
2538 176         550 return(0);
2539             }
2540              
2541             sub _err_res
2542             {
2543 85     85   175 my( $ctx, $schema_ptr, $msg, $keyword ) = @_;
2544 85         231 _err( $ctx, $schema_ptr, $msg, $keyword );
2545 85         370 return( { ok => 0, props => {}, items => {} } );
2546             }
2547              
2548             # Used by _prune* methods
2549             sub _extract_array_shape
2550             {
2551 1     1   2 my( $self, $schema, $out ) = @_;
2552              
2553 1 50       4 return unless( ref( $schema ) eq 'HASH' );
2554              
2555 1 50       4 if( ref( $schema->{prefixItems} ) eq 'ARRAY' )
2556             {
2557 1         3 my $src = $schema->{prefixItems};
2558 1         5 for my $i ( 0 .. $#$src )
2559             {
2560             # First win: do not override an existing prefix schema at this index
2561             $out->{prefix_items}->[ $i ] = $src->[ $i ]
2562 1 50       6 unless( defined( $out->{prefix_items}->[ $i ] ) );
2563             }
2564             }
2565              
2566 1 50 33     8 if( exists( $schema->{items} ) && ref( $schema->{items} ) eq 'HASH' )
2567             {
2568             # Again, first win: if we already have items from another branch, keep it.
2569 1 50       41 $out->{items} = $schema->{items} unless( $out->{items} );
2570             }
2571              
2572             # allOf mixins for arrays as well
2573 1 50       8 if( ref( $schema->{allOf} ) eq 'ARRAY' )
2574             {
2575 0         0 foreach my $sub ( @{$schema->{allOf}} )
  0         0  
2576             {
2577 0         0 $self->_extract_array_shape( $sub, $out );
2578             }
2579             }
2580              
2581             # anyOf / oneOf / not ignored for same reason as objects.
2582             }
2583              
2584             # Used by _prune* methods
2585             sub _extract_object_shape
2586             {
2587 8     8   15 my( $self, $schema, $out ) = @_;
2588              
2589 8 50       25 return unless( ref( $schema ) eq 'HASH' );
2590              
2591             # Direct properties
2592 8 100       19 if( ref( $schema->{properties} ) eq 'HASH' )
2593             {
2594 6         8 foreach my $k ( keys( %{$schema->{properties}} ) )
  6         20  
2595             {
2596             # First win: do not override an already-collected subschema
2597             $out->{props}->{ $k } = $schema->{properties}->{ $k }
2598 7 50       27 unless( exists( $out->{props}->{ $k } ) );
2599             }
2600             }
2601              
2602             # patternProperties
2603 8 100       23 if( ref( $schema->{patternProperties} ) eq 'HASH' )
2604             {
2605 1         2 foreach my $re ( keys( %{$schema->{patternProperties}} ) )
  1         4  
2606             {
2607 1         3 push( @{$out->{patterns}}, [ $re, $schema->{patternProperties}->{ $re } ] );
  1         5  
2608             }
2609             }
2610              
2611             # additionalProperties
2612 8 100       31 if( exists( $schema->{additionalProperties} ) )
2613             {
2614 7         13 my $ap = $schema->{additionalProperties};
2615              
2616             # JSON booleans or plain scalars
2617 7 50 33     64 if( !ref( $ap ) || ( blessed( $ap ) && $ap->isa( 'JSON::PP::Boolean' ) ) )
    0 33        
2618             {
2619 7 100       65 if( $ap )
2620             {
2621             # true: additionalProperties allowed; keep any stricter setting we might already have
2622 1 50 33     16 $out->{allow_ap} = 1 unless( defined( $out->{allow_ap} ) && !$out->{allow_ap} );
2623             # do not touch ap_schema here
2624             }
2625             else
2626             {
2627             # false: forbidden regardless of earlier "true"
2628 6         58 $out->{allow_ap} = 0;
2629 6         13 $out->{ap_schema} = undef;
2630             }
2631             }
2632             elsif( ref( $ap ) eq 'HASH' )
2633             {
2634             # Schema for additionals
2635 0         0 $out->{allow_ap} = 1;
2636 0         0 $out->{ap_schema} = $ap;
2637             }
2638             }
2639              
2640             # allOf mixins: merge their object keywords as well.
2641 8 100       26 if( ref( $schema->{allOf} ) eq 'ARRAY' )
2642             {
2643 1         3 foreach my $sub ( @{$schema->{allOf}} )
  1         5  
2644             {
2645 2         9 $self->_extract_object_shape( $sub, $out );
2646             }
2647             }
2648              
2649             # NOTE:
2650             # We intentionally ignore anyOf / oneOf / not for pruning.
2651             # Without performing full validation against each branch, we cannot
2652             # safely decide which properties are truly forbidden, so we err on
2653             # the side of *keeping* more rather than over-pruning.
2654             }
2655              
2656 91     91   537 sub _fail { return( { ok => 0, props => {}, items => {} } ); }
2657              
2658             sub _first_error_text
2659             {
2660 0     0   0 my( $errs ) = @_;
2661 0 0       0 return( '' ) unless( @$errs );
2662 0         0 my $e = $errs->[0];
2663 0         0 return( "$e" );
2664             }
2665              
2666             sub _get_args_as_hash
2667             {
2668 341     341   620 my $self = shift( @_ );
2669 341 100       1160 return( {} ) if( !scalar( @_ ) );
2670 21         52 my $ref = {};
2671 21 100 66     187 if( scalar( @_ ) == 1 &&
    50 50        
      66        
2672             defined( $_[0] ) &&
2673             ( ref( $_[0] ) || '' ) eq 'HASH' )
2674             {
2675 2         5 $ref = shift( @_ );
2676             }
2677             elsif( !( scalar( @_ ) % 2 ) )
2678             {
2679 19         85 $ref = { @_ };
2680             }
2681             else
2682             {
2683 0         0 die( "Uneven number of parameters provided: '", join( "', '", map( overload::StrVal( $_ ), @_ ) ), "'" );
2684             }
2685 21         51 return( $ref );
2686             }
2687              
2688             sub _index_schema_202012
2689             {
2690 1999     1999   3310 my( $node, $base_uri, $ptr, $anchors, $id_index ) = @_;
2691              
2692 1999         5074 $anchors->{ $ptr } = $node;
2693              
2694 1999         2181 my $here_base = $base_uri;
2695              
2696 1999 100       5088 if( ref( $node ) eq 'HASH' )
    100          
2697             {
2698 790 50 66     1655 if( exists( $node->{'$id'} ) && defined( $node->{'$id'} ) && $node->{'$id'} ne '' )
      66        
2699             {
2700 15         56 $here_base = _resolve_uri( $base_uri, $node->{'$id'} );
2701 15         45 $id_index->{ $here_base } = $node;
2702             }
2703              
2704 790 50 66     1549 if( exists( $node->{'$anchor'} ) && defined( $node->{'$anchor'} ) && $node->{'$anchor'} ne '' )
      66        
2705             {
2706 1         3 my $abs = $here_base . '#' . $node->{'$anchor'};
2707 1         3 $id_index->{ $abs } = $node;
2708             }
2709              
2710 790 50 66     1379 if( exists( $node->{'$dynamicAnchor'} ) && defined( $node->{'$dynamicAnchor'} ) && $node->{'$dynamicAnchor'} ne '' )
      66        
2711             {
2712 2         7 my $abs = $here_base . '#dyn:' . $node->{'$dynamicAnchor'};
2713 2         33 $id_index->{ $abs } = $node;
2714             }
2715              
2716 790         2200 for my $k ( sort( keys( %$node ) ) )
2717             {
2718 1569         2146 my $child = $node->{ $k };
2719 1569         2246 my $child_ptr = _join_ptr( $ptr, $k );
2720 1569         3084 _index_schema_202012( $child, $here_base, $child_ptr, $anchors, $id_index );
2721             }
2722             }
2723             elsif( ref( $node ) eq 'ARRAY' )
2724             {
2725 233         515 for my $i ( 0 .. $#$node )
2726             {
2727 311         489 my $child = $node->[$i];
2728 311         437 my $child_ptr = _join_ptr( $ptr, $i );
2729 311         586 _index_schema_202012( $child, $here_base, $child_ptr, $anchors, $id_index );
2730             }
2731             }
2732             }
2733              
2734             sub _inst_addr
2735             {
2736 364     364   605 my( $inst, $ptr ) = @_;
2737 364 50       688 return( "SCALAR:$ptr" ) unless( ref( $inst ) );
2738 364         1119 return( ref( $inst ) . ':' . refaddr( $inst ) );
2739             }
2740              
2741             # truthy helpers
2742 28 100   28   244 sub _is_hash { my $v = shift; return ref($v) eq 'HASH' ? 1 : 0; }
  28         144  
2743              
2744             sub _is_number
2745             {
2746 923     923   1533 my( $v ) = @_;
2747              
2748 923 100       2004 return(0) if( ref( $v ) );
2749 588 100       996 return(0) unless( defined( $v ) );
2750              
2751             # Strict JSON typing: accept only scalars that actually carry numeric flags.
2752             # JSON marks numbers with IOK/NOK; plain strings (even "12") will not have them.
2753 582         2236 my $sv = B::svref_2object( \$v );
2754 582         2656 my $flags = $sv->FLAGS;
2755              
2756 582         856 local $@;
2757             # SVf_IOK = 0x02000000, SVf_NOK = 0x04000000 on most builds;
2758             # we do not hardcode constants—B::SV’s FLAGS is stable to test with these bitmasks.
2759             # Use string eval to avoid importing platform-specific constants.
2760 582   50     868 my $SVf_IOK = eval{ B::SVf_IOK() } || 0x02000000;
2761 582   50     738 my $SVf_NOK = eval{ B::SVf_NOK() } || 0x04000000;
2762              
2763 582 100       2650 return( ( $flags & ( $SVf_IOK | $SVf_NOK ) ) ? 1 : 0 );
2764             }
2765              
2766 28 50   28   46 sub _is_true { my $v = shift( @_ ); return( ref( $v ) eq 'HASH' ? 0 : $v ? 1 : 0 ); }
  28 100       399  
2767              
2768             sub _join_ptr
2769             {
2770 2917     2917   4781 my( $base, @tokens ) = @_;
2771              
2772             # Default base to '#' if not provided
2773 2917 50 33     8351 $base = '#' unless( defined( $base ) && length( $base ) );
2774              
2775 2917         3297 my $ptr = $base;
2776              
2777 2917         3464 for my $token ( @tokens )
2778             {
2779 2917 50       4394 next unless( defined( $token ) );
2780              
2781             # Proper rfc6901 JSON Pointer escaping
2782 2917         4187 $token =~ s/~/~0/g;
2783 2917         4129 $token =~ s/\//~1/g;
2784              
2785 2917 100       4954 if( $ptr eq '#' )
2786             {
2787 1015         1904 $ptr = "#/$token";
2788             }
2789             else
2790             {
2791 1902         3096 $ptr .= "/$token";
2792             }
2793             }
2794              
2795 2917         6016 return( $ptr );
2796             }
2797              
2798             sub _js_quote
2799             {
2800 0     0   0 my $s = shift( @_ );
2801 0 0       0 $s = '' unless( defined( $s ) );
2802 0         0 $s =~ s/\\/\\\\/g;
2803 0         0 $s =~ s/'/\\'/g;
2804 0         0 $s =~ s/\r\n/\n/g;
2805 0         0 $s =~ s/\r/\n/g;
2806 0         0 $s =~ s/\n/\\n/g;
2807 0         0 return( "'$s'" );
2808             }
2809              
2810             sub _json_equal
2811             {
2812 21     21   27 my( $a, $b ) = @_;
2813 21         68 return( _canon( $a ) eq _canon( $b ) );
2814             }
2815              
2816             # Very small JSON Pointer resolver for internal refs ("#/...") for JS compile
2817             sub _jsv_resolve_internal_ref
2818             {
2819 9     9   16 my( $root, $ptr ) = @_;
2820              
2821 9 50 33     36 return( $root ) if( !defined( $ptr ) || $ptr eq '' || $ptr eq '#' );
      33        
2822              
2823             # Expect something like "#/definitions/address"
2824 9         23 $ptr =~ s/^#//;
2825              
2826 9         34 my @tokens = split( /\//, $ptr );
2827 9 50 33     26 shift( @tokens ) if( @tokens && $tokens[0] eq '' );
2828              
2829 9         11 my $node = $root;
2830              
2831             TOKEN:
2832 9         14 for my $tok ( @tokens )
2833             {
2834             # rfc6901 JSON Pointer unescaping
2835 17         18 $tok =~ s/~1/\//g;
2836 17         22 $tok =~ s/~0/~/g;
2837              
2838 17 50       23 if( ref( $node ) eq 'HASH' )
    0          
2839             {
2840             # Draft 2020-12 alias: allow "definitions" to hit "$defs" if needed
2841 17 100 66     55 if( $tok eq 'definitions' &&
      100        
2842             !exists( $node->{definitions} ) &&
2843             exists( $node->{'$defs'} ) )
2844             {
2845 8         12 $tok = '$defs';
2846             }
2847              
2848 17 100       24 unless( exists( $node->{ $tok } ) )
2849             {
2850             # Optional: help debug resolution problems
2851 1 50       3 warn( "_jsv_resolve_internal_ref: token '$tok' not found in current hash for pointer '$ptr'\n" )
2852             if( $JSON::Schema::Validate::DEBUG );
2853 1         3 return;
2854             }
2855 16         22 $node = $node->{ $tok };
2856             }
2857             elsif( ref( $node ) eq 'ARRAY' )
2858             {
2859 0 0 0     0 unless( $tok =~ /^\d+$/ && $tok < @$node )
2860             {
2861 0 0       0 warn( "_jsv_resolve_internal_ref: array index '$tok' out of range for pointer '$ptr'\n" )
2862             if( $JSON::Schema::Validate::DEBUG );
2863 0         0 return;
2864             }
2865 0         0 $node = $node->[ $tok ];
2866             }
2867             else
2868             {
2869 0 0       0 warn( "_jsv_resolve_internal_ref: reached non-container node while resolving '$ptr'\n" )
2870             if( $JSON::Schema::Validate::DEBUG );
2871 0         0 return;
2872             }
2873             }
2874              
2875 8         14 return( $node );
2876             }
2877              
2878             # Keyword groups
2879             sub _k_array_all
2880             {
2881 32     32   81 my( $ctx, $sp, $S, $A ) = @_;
2882              
2883 32 50 66     112 if( exists( $S->{minItems} ) && @$A < $S->{minItems} )
2884             {
2885 0         0 return( _err_res( $ctx, $sp, "array has fewer than minItems $S->{minItems}", 'minItems' ) );
2886             }
2887 32 50 33     115 if( exists( $S->{maxItems} ) && @$A > $S->{maxItems} )
2888             {
2889 0         0 return( _err_res( $ctx, $sp, "array has more than maxItems $S->{maxItems}", 'maxItems' ) );
2890             }
2891              
2892 32 50       86 if( $S->{uniqueItems} )
2893             {
2894 0         0 my %seen;
2895 0         0 for my $i ( 0 .. $#$A )
2896             {
2897 0         0 my $k = _canon( $A->[$i] );
2898 0 0       0 if( $seen{ $k }++ )
2899             {
2900 0         0 return( _err_res( $ctx, _join_ptr( $sp, $i ), "array items not unique", 'uniqueItems' ) );
2901             }
2902             }
2903             }
2904              
2905 32         49 my %items_ann;
2906              
2907 32 100       197 if( ref( $S->{prefixItems} ) eq 'ARRAY' )
    100          
2908             {
2909 5         9 my $tuple = $S->{prefixItems};
2910 5         15 for my $i ( 0 .. $#$A )
2911             {
2912 16         19 push( @{$ctx->{ptr_stack}}, _join_ptr( $sp, $i ) );
  16         34  
2913              
2914 16 100 66     70 if( $i <= $#$tuple )
    100          
2915             {
2916 10         43 my $r = _v( $ctx, _join_ptr( $sp, "prefixItems/$i" ), $tuple->[$i], $A->[$i] );
2917 10 50       26 return( $r ) unless( $r->{ok} );
2918 10         51 $items_ann{ $i } = 1;
2919             }
2920             elsif( exists( $S->{items} ) && ref( $S->{items} ) eq 'HASH' )
2921             {
2922 5         10 my $r = _v( $ctx, _join_ptr( $sp, "items" ), $S->{items}, $A->[$i] );
2923 5 100       13 return( $r ) unless( $r->{ok} );
2924 4         14 $items_ann{ $i } = 1;
2925             }
2926              
2927 15         16 pop( @{$ctx->{ptr_stack}} );
  15         38  
2928             }
2929             }
2930             elsif( ref( $S->{items} ) eq 'HASH' )
2931             {
2932 14         46 for my $i ( 0 .. $#$A )
2933             {
2934 27         33 push( @{$ctx->{ptr_stack}}, _join_ptr( $sp, $i ) );
  27         60  
2935 27         45 my $r = _v( $ctx, _join_ptr( $sp, "items" ), $S->{items}, $A->[$i] );
2936 27 100       87 return( $r ) unless( $r->{ok} );
2937 25         60 $items_ann{ $i } = 1;
2938 25         29 pop( @{$ctx->{ptr_stack}} );
  25         97  
2939             }
2940             }
2941              
2942 29 100       83 if( exists( $S->{contains} ) )
2943             {
2944 10         16 my $matches = 0;
2945              
2946             # Quiet sub-context to avoid emitting errors for non-matching items
2947 10         35 for my $i ( 0 .. $#$A )
2948             {
2949 29         301 my %shadow = %$ctx;
2950 29         99 my @errs;
2951 29         48 $shadow{errors} = \@errs;
2952 29         41 $shadow{error_count} = 0;
2953              
2954 29         81 my $tmp = _v( \%shadow, _join_ptr( $sp, "contains" ), $S->{contains}, $A->[$i] );
2955 29 100       248 $matches++ if( $tmp->{ok} );
2956             }
2957              
2958 10 50       31 my $minc = defined( $S->{minContains} ) ? $S->{minContains} : 1;
2959 10 100       27 my $maxc = defined( $S->{maxContains} ) ? $S->{maxContains} : ( 2**31 - 1 );
2960              
2961 10 100       41 return( _err_res( $ctx, $sp, "contains matched $matches < minContains $minc", 'minContains' ) ) if( $matches < $minc );
2962 7 100       31 return( _err_res( $ctx, $sp, "contains matched $matches > maxContains $maxc", 'maxContains' ) ) if( $matches > $maxc );
2963             }
2964              
2965 24 100       64 if( exists( $S->{unevaluatedItems} ) )
2966             {
2967 3         6 my @unknown = ();
2968 3         10 for my $i ( 0 .. $#$A )
2969             {
2970 10 100       32 next if( $items_ann{ $i } );
2971 1         3 push( @unknown, $i );
2972             }
2973 3         6 my $UE = $S->{unevaluatedItems};
2974 3 50 33     9 if( !_is_true( $UE ) && !_is_hash( $UE ) )
    0          
2975             {
2976 3 100       22 return( _err_res( $ctx, $sp, "unevaluatedItems not allowed at indices: " . join( ',', @unknown ), 'unevaluatedItems' ) ) if( @unknown );
2977             }
2978             elsif( ref( $UE ) eq 'HASH' )
2979             {
2980 0         0 for my $i ( @unknown )
2981             {
2982 0         0 my $r = _v( $ctx, _join_ptr( $sp, "unevaluatedItems" ), $UE, $A->[$i] );
2983 0 0       0 return( $r ) unless( $r->{ok} );
2984 0         0 $items_ann{ $i } = 1;
2985             }
2986             }
2987             }
2988              
2989 23         88 return( { ok => 1, props => {}, items => \%items_ann } );
2990             }
2991              
2992             sub _k_combinator
2993             {
2994 25     25   44 my( $ctx, $sp, $S, $inst, $kw ) = @_;
2995              
2996              
2997             # allOf: all subschemas must pass, we merge their annotations
2998 25 100       45 if( $kw eq 'allOf' )
2999             {
3000 10         12 my %props;
3001             my %items;
3002              
3003 10         8 for my $i ( 0 .. $#{$S->{allOf}} )
  10         23  
3004             {
3005 16         43 my $r = _v( $ctx, _join_ptr( $sp, "allOf/$i" ), $S->{allOf}->[ $i ], $inst );
3006 16 100       34 return( $r ) unless( $r->{ok} );
3007              
3008 13         19 %props = ( %props, %{$r->{props}} );
  13         18  
3009 13         15 %items = ( %items, %{$r->{items}} );
  13         26  
3010             }
3011              
3012 7         22 return( { ok => 1, props => \%props, items => \%items } );
3013             }
3014              
3015             # anyOf: at least one subschema must pass.
3016             # Errors from failing branches must NOT leak into the main context.
3017 15 100       31 if( $kw eq 'anyOf' )
3018             {
3019 2         5 my @branch_errs;
3020              
3021 2         2 for my $i ( 0 .. $#{$S->{anyOf}} )
  2         6  
3022             {
3023             # Shadow context: isolate errors for this branch
3024 3         20 my %shadow = %$ctx;
3025 3         5 my @errs;
3026 3         5 $shadow{errors} = \@errs;
3027 3         6 $shadow{error_count} = 0;
3028              
3029 3         9 my $r = _v( \%shadow, _join_ptr( $sp, "anyOf/$i" ), $S->{anyOf}->[ $i ], $inst );
3030              
3031 3 100       7 if( $r->{ok} )
3032             {
3033             # One branch passed: combinator satisfied.
3034             # Ignore all other branch errors.
3035 1         6 return( { ok => 1, props => {}, items => {} } );
3036             }
3037              
3038 2         9 push( @branch_errs, \@errs );
3039             }
3040              
3041             # No branch matched: merge collected branch errors into main context
3042 1         2 for my $aref ( @branch_errs )
3043             {
3044 2 50       4 next unless( @$aref );
3045 2         3 push( @{$ctx->{errors}}, @$aref );
  2         4  
3046 2         2 $ctx->{error_count} += scalar( @$aref );
3047             }
3048              
3049 1         3 return( _err_res( $ctx, $sp, "instance does not satisfy anyOf", 'anyOf' ) );
3050             }
3051              
3052             # oneOf: exactly one subschema must pass.
3053             # Again, do not leak errors from non-selected branches.
3054 13 100       29 if( $kw eq 'oneOf' )
3055             {
3056 11         16 my @ok_results;
3057             my @branch_errs;
3058              
3059 11         16 for my $i ( 0 .. $#{$S->{oneOf}} )
  11         34  
3060             {
3061 22         261 my %shadow = %$ctx;
3062 22         55 my @errs;
3063 22         33 $shadow{errors} = \@errs;
3064 22         31 $shadow{error_count} = 0;
3065              
3066 22         60 my $r = _v( \%shadow, _join_ptr( $sp, "oneOf/$i" ), $S->{oneOf}->[$i], $inst );
3067              
3068 22 100       47 if( $r->{ok} )
3069             {
3070 6         31 push( @ok_results, $r );
3071             }
3072             else
3073             {
3074 16         88 push( @branch_errs, \@errs );
3075             }
3076             }
3077              
3078 11 100       28 if( @ok_results == 1 )
3079             {
3080             # Exactly one branch matched: combinator satisfied.
3081             # Do NOT bubble up branch props/items through oneOf.
3082 4         38 return( { ok => 1, props => {}, items => {} } );
3083             }
3084              
3085             # Zero or >1 matched -> failure; merge branch errors
3086 7         11 for my $aref ( @branch_errs )
3087             {
3088 12 50       22 next unless( @$aref );
3089 12         12 push( @{$ctx->{errors}}, @$aref );
  12         37  
3090 12         50 $ctx->{error_count} += scalar( @$aref );
3091             }
3092              
3093             return(
3094 7         26 _err_res(
3095             $ctx,
3096             $sp,
3097             "instance satisfies " . scalar( @ok_results ) . " schemas in oneOf (expected exactly 1)",
3098             'oneOf'
3099             )
3100             );
3101             }
3102              
3103             # not: subschema must NOT validate.
3104             # Any errors from validating the inner schema are irrelevant on success.
3105 2 50       5 if( $kw eq 'not' )
3106             {
3107 2         18 my %shadow = %$ctx;
3108 2         5 my @errs;
3109 2         4 $shadow{errors} = \@errs;
3110 2         3 $shadow{error_count} = 0;
3111              
3112 2         5 my $r = _v( \%shadow, _join_ptr( $sp, "not" ), $S->{not}, $inst );
3113              
3114             # If inner schema passes, then "not" fails
3115             return( _err_res( $ctx, $sp, "instance matches forbidden not-schema", 'not' ) )
3116 2 100       15 if( $r->{ok} );
3117              
3118             # Otherwise, "not" is satisfied; ignore inner errors entirely
3119 1         8 return( { ok => 1, props => {}, items => {} } );
3120             }
3121              
3122             # Unknown / unsupported combinator (defensive default)
3123 0         0 return( { ok => 1, props => {}, items => {} } );
3124             }
3125              
3126             sub _k_const
3127             {
3128 6     6   11 my( $ctx, $v, $const, $ptr ) = @_;
3129 6 50       15 return(1) if( _json_equal( $v, $const ) );
3130 0         0 return( _err( $ctx, $ptr, "const mismatch", 'const' ) );
3131             }
3132              
3133             sub _k_enum
3134             {
3135 8     8   21 my( $ctx, $v, $arr, $ptr ) = @_;
3136 8         9 for my $e ( @$arr )
3137             {
3138 15 100       21 return(1) if( _json_equal( $v, $e ) );
3139             }
3140 4         15 return( _err( $ctx, $ptr, "value not in enum", 'enum' ) );
3141             }
3142              
3143             sub _k_format
3144             {
3145 55     55   129 my( $ctx, $s, $fmt, $ptr ) = @_;
3146 55         166 my $cb = $ctx->{formats}->{ $fmt };
3147 55 100       172 return(1) unless( $cb );
3148 52         73 local $@;
3149 52 100       82 my $ok = eval{ $cb->( $s ) ? 1 : 0 };
  52         159  
3150 52 100       1261 return( $ok ? 1 : _err( $ctx, $ptr, "string fails format '$fmt'", 'format' ) );
3151             }
3152              
3153             sub _k_if_then_else
3154             {
3155 16     16   23 my( $ctx, $sp, $S, $inst ) = @_;
3156              
3157             # Evaluate "if" in a shadow context so its errors do NOT leak
3158 16         185 my %shadow = %$ctx;
3159 16         34 my @errs;
3160 16         19 $shadow{errors} = \@errs;
3161 16         20 $shadow{error_count} = 0;
3162              
3163 16         24 my $cond = _v( \%shadow, _join_ptr( $sp, 'if' ), $S->{if}, $inst );
3164              
3165 16 100       27 if( $cond->{ok} )
3166             {
3167 6 50       14 _t( $ctx, $sp, 'if', undef, 'pass', 'then' ) if( $ctx->{trace_on} );
3168             return( { ok => 1, props => {}, items => {} } )
3169 6 50       11 unless( exists( $S->{then} ) );
3170              
3171             # Apply "then" against the REAL context
3172 6         11 return( _v( $ctx, _join_ptr( $sp, 'then' ), $S->{then}, $inst ) );
3173             }
3174             else
3175             {
3176 10 50       21 _t( $ctx, $sp, 'if', undef, 'pass', 'else' ) if( $ctx->{trace_on} );
3177             return( { ok => 1, props => {}, items => {} } )
3178 10 50       99 unless( exists( $S->{else} ) );
3179              
3180             # Apply "else" against the REAL context
3181 0         0 return( _v( $ctx, _join_ptr( $sp, 'else' ), $S->{else}, $inst ) );
3182             }
3183             }
3184              
3185             sub _k_number
3186             {
3187 88     88   262 my( $ctx, $v, $kw, $arg, $ptr ) = @_;
3188 88 100       327 if( $kw eq 'multipleOf' )
    100          
    100          
    100          
    50          
3189             {
3190             # Guard per spec: multipleOf must be > 0
3191 13 100 66     77 if( !defined( $arg ) || $arg <= 0 )
3192             {
3193 1 50       5 _t( $ctx, $ptr, 'multipleOf', undef, 'fail', 'arg<=0' ) if( $ctx->{trace_on} );
3194 1         4 return( _err( $ctx, $ptr, "multipleOf must be > 0", 'multipleOf' ) );
3195             }
3196             # Float-tolerant multiple check
3197             # my $ok = abs( ( $v / $arg ) - int( $v / $arg + 1e-10 ) ) < 1e-9;
3198 12         57 my $ok = abs( ( $v / $arg ) - int( $v / $arg + 0.0000000001 ) ) < 1e-9;
3199 12 0       28 _t( $ctx, $ptr, 'multipleOf', undef, $ok ? 'pass' : 'fail', "$v mod $arg" ) if( $ctx->{trace_on} );
    50          
3200 12 100       62 return( $ok ? 1 : _err( $ctx, $ptr, "number not multipleOf $arg" ) );
3201             }
3202             elsif( $kw eq 'minimum' )
3203             {
3204 62 50       177 _t( $ctx, $ptr, 'minimum', undef, $v >= $arg ? 'pass' : 'fail', "$v >= $arg" ) if( $ctx->{trace_on} );
    100          
3205 62 100       307 return( $v >= $arg ? 1 : _err( $ctx, $ptr, "number less than minimum $arg", 'minimum' ) );
3206             }
3207             elsif( $kw eq 'maximum' )
3208             {
3209 6 0       19 _t( $ctx, $ptr, 'maximum', undef, $v <= $arg ? 'pass' : 'fail', "$v <= $arg" ) if( $ctx->{trace_on} );
    50          
3210 6 100       31 return( $v <= $arg ? 1 : _err( $ctx, $ptr, "number greater than maximum $arg", 'maximum' ) );
3211             }
3212             elsif( $kw eq 'exclusiveMinimum' )
3213             {
3214 5 0       13 _t( $ctx, $ptr, 'exclusiveMinimum', undef, $v > $arg ? 'pass' : 'fail', "$v > $arg" ) if( $ctx->{trace_on} );
    50          
3215 5 100       42 return( $v > $arg ? 1 : _err( $ctx, $ptr, "number not greater than exclusiveMinimum $arg", 'exclusiveMinimum' ) );
3216             }
3217             elsif( $kw eq 'exclusiveMaximum' )
3218             {
3219 2 0       6 _t( $ctx, $ptr, 'exclusiveMaximum', undef, $v < $arg ? 'pass' : 'fail', "$v < $arg" ) if( $ctx->{trace_on} );
    50          
3220 2 100       14 return( $v < $arg ? 1 : _err( $ctx, $ptr, "number not less than exclusiveMaximum $arg", 'exclusiveMaximum' ) );
3221             }
3222 0         0 return(1);
3223             }
3224              
3225             sub _k_object_all
3226             {
3227 287     287   541 my( $ctx, $sp, $S, $H ) = @_;
3228              
3229 287         318 my $ok = 1;
3230              
3231             my $bail_if_max = sub
3232             {
3233 136 100 66 136   848 return( $ctx->{max_errors} && $ctx->{error_count} >= $ctx->{max_errors} ) ? 1 : 0;
3234 287         1414 };
3235              
3236 287 50 33     885 if( exists( $S->{minProperties} ) && ( scalar( keys( %$H ) ) ) < $S->{minProperties} )
3237             {
3238 0         0 _err_res( $ctx, $sp, "object has fewer than minProperties $S->{minProperties}", 'minProperties' );
3239 0         0 $ok = 0;
3240 0 0       0 return( { ok => 0, props => {}, items => {} } ) if( $bail_if_max->() );
3241             }
3242 287 50 33     687 if( exists( $S->{maxProperties} ) && ( scalar( keys( %$H ) ) ) > $S->{maxProperties} )
3243             {
3244 0         0 _err_res( $ctx, $sp, "object has more than maxProperties $S->{maxProperties}", 'maxProperties' );
3245 0         0 $ok = 0;
3246 0 0       0 return( { ok => 0, props => {}, items => {} } ) if( $bail_if_max->() );
3247             }
3248              
3249             # Merge required from:
3250             # - top-level "required" (array only)
3251             # - property-level { required => 1 } or { optional => 0 }
3252 287         377 my %required;
3253              
3254 287 100 100     973 if( exists( $S->{required} ) && ref( $S->{required} ) eq 'ARRAY' )
3255             {
3256 162         193 $required{ $_ } = 1 for( @{ $S->{required} } );
  162         625  
3257             }
3258              
3259 287 100       855 if( my $P = $S->{properties} )
3260             {
3261 251         608 for my $k ( keys( %$P ) )
3262             {
3263 393         588 my $pd = $P->{ $k };
3264 393 50       720 next unless( ref( $pd ) eq 'HASH' );
3265              
3266 393 100       717 if( exists( $pd->{required} ) )
3267             {
3268 50 100       101 $required{ $k } = $pd->{required} ? 1 : 0;
3269             }
3270 393 100       831 if( exists( $pd->{optional} ) )
3271             {
3272 4 100       17 $required{ $k } = $pd->{optional} ? 0 : 1; # optional => 0 means required
3273             }
3274             }
3275             }
3276              
3277 287         701 for my $rq ( grep{ $required{ $_ } } keys( %required ) )
  263         595  
3278             {
3279 257 100       539 next if( exists( $H->{ $rq } ) );
3280 39 100       80 _t( $ctx,$sp, 'required', undef, 'fail', $rq ) if( $ctx->{trace_on} );
3281              
3282 39         64 my @need = sort grep { $required{ $_ } } keys %required;
  54         138  
3283 39         98 my @have = sort keys %$H;
3284              
3285 39 50       123 my $need_str = @need ? join( ', ', @need ) : '(none)';
3286 39 100       86 my $have_str = @have ? join( ', ', @have ) : '(none)';
3287              
3288 39         120 my $msg = "required property '$rq' is missing "
3289             . "(required: $need_str; present: $have_str)";
3290              
3291 39         70 _err_res(
3292             $ctx,
3293             _join_ptr( $sp, $rq ),
3294             $msg,
3295             'required'
3296             );
3297              
3298 39         84 $ok = 0;
3299 39 100       60 return( { ok => 0, props => {}, items => {} } ) if( $bail_if_max->() );
3300             }
3301              
3302 286 100 66     699 if( exists( $S->{propertyNames} ) && ref( $S->{propertyNames} ) eq 'HASH' )
3303             {
3304 6         10 for my $k ( keys( %$H ) )
3305             {
3306 12         27 my $r = _v( $ctx, _join_ptr( $sp, "propertyNames" ), $S->{propertyNames}, $k );
3307 12 100       34 if( !$r->{ok} )
3308             {
3309 2         3 $ok = 0;
3310 2 50       5 return( { ok => 0, props => {}, items => {} } ) if( $bail_if_max->() );
3311             }
3312             }
3313             }
3314              
3315 286   100     650 my $props = $S->{properties} || {};
3316 286   100     913 my $patprops = $S->{patternProperties} || {};
3317 286         491 my $addl_set = exists( $S->{additionalProperties} );
3318 286 100       932 my $addl = $addl_set ? $S->{additionalProperties} : JSON::true;
3319              
3320 286         554 my %ann;
3321              
3322 286         817 for my $k ( sort( keys( %$H ) ) )
3323             {
3324 443         638 my $v = $H->{ $k };
3325 443         459 my $matched = 0;
3326              
3327 443         775 my $child_path = _join_ptr( $sp, $k );
3328 443         561 push( @{$ctx->{ptr_stack}}, $child_path );
  443         931  
3329              
3330 443 100       793 if( exists( $props->{ $k } ) )
3331             {
3332 288         791 my $r = _v( $ctx, _join_ptr( $sp, "properties/$k" ), $props->{ $k }, $v );
3333 288 100       720 if( !$r->{ok} )
3334             {
3335 74         150 $ok = 0;
3336 74         112 pop( @{$ctx->{ptr_stack}} );
  74         145  
3337 74 100       294 return( { ok => 0, props => \%ann, items => {} } ) if( $bail_if_max->() );
3338 73         294 next;
3339             }
3340 214         424 $ann{ $k } = 1;
3341 214         601 $matched = 1;
3342             }
3343              
3344 369 100       575 unless( $matched )
3345             {
3346 155         167 local $@;
3347 155         235 for my $re ( keys( %$patprops ) )
3348             {
3349 4         28 my $re_ok = eval{ $k =~ /$re/ };
  4         47  
3350 4 100       14 next unless( $re_ok );
3351              
3352 2         7 my $r = _v( $ctx, _join_ptr( $sp, "patternProperties/$re" ), $patprops->{ $re }, $v );
3353 2 50       7 if( !$r->{ok} )
3354             {
3355 0         0 $ok = 0;
3356 0         0 pop( @{$ctx->{ptr_stack}} );
  0         0  
3357 0 0       0 return( { ok => 0, props => \%ann, items => {} } ) if( $bail_if_max->() );
3358 0         0 next;
3359             }
3360 2         5 $ann{ $k } = 1;
3361 2         9 $matched = 1;
3362             }
3363             }
3364              
3365 369 100       629 unless( $matched )
3366             {
3367 153 100 66     437 if( $addl_set && !_is_true( $addl ) && !_is_hash( $addl ) )
    100 100        
3368             {
3369 16         30 _err_res( $ctx, _join_ptr( $sp, $k ), "additionalProperties not allowed: '$k'", 'additionalProperties' );
3370 16         29 $ok = 0;
3371 16         18 pop( @{$ctx->{ptr_stack}} );
  16         66  
3372 16 50       50 return( { ok => 0, props => \%ann, items => {} } ) if( $bail_if_max->() );
3373 16         35 next;
3374             }
3375             elsif( ref( $addl ) eq 'HASH' )
3376             {
3377 5         47 my $r = _v( $ctx, _join_ptr( $sp, "additionalProperties" ), $addl, $v );
3378 5 50       18 if( !$r->{ok} )
3379             {
3380 0         0 $ok = 0;
3381 0         0 pop( @{$ctx->{ptr_stack}} );
  0         0  
3382 0 0       0 return( { ok => 0, props => \%ann, items => {} } ) if( $bail_if_max->() );
3383 0         0 next;
3384             }
3385 5         22 $ann{ $k } = 1;
3386             }
3387             }
3388              
3389 353         358 pop( @{$ctx->{ptr_stack}} );
  353         793  
3390             }
3391              
3392 285 100       895 if( my $depR = $S->{dependentRequired} )
3393             {
3394 8         17 for my $k ( keys( %$depR ) )
3395             {
3396 8 100       19 next unless( exists( $H->{ $k } ) );
3397 6 50       7 for my $need ( @{$depR->{ $k } || []} )
  6         16  
3398             {
3399 6 100       16 next if( exists( $H->{ $need } ) );
3400 2         6 _err_res( $ctx, _join_ptr( $sp, $need ), "dependentRequired: '$need' required when '$k' is present", 'dependentRequired' );
3401 2         6 $ok = 0;
3402 2 50       7 return( { ok => 0, props => \%ann, items => {} } ) if( $bail_if_max->() );
3403             }
3404             }
3405             }
3406              
3407 285 100       622 if( my $depS = $S->{dependentSchemas} )
3408             {
3409 8         17 for my $k ( keys( %$depS ) )
3410             {
3411 8 100       45 next unless( exists( $H->{ $k } ) );
3412 5         16 my $r = _v( $ctx, _join_ptr( $sp, "dependentSchemas/$k" ), $depS->{ $k }, $H );
3413 5 100       21 if( !$r->{ok} )
3414             {
3415 2         5 $ok = 0;
3416 2 50       6 return( { ok => 0, props => \%ann, items => {} } ) if( $bail_if_max->() );
3417             }
3418             }
3419             }
3420              
3421 285 100       610 if( exists( $S->{unevaluatedProperties} ) )
3422             {
3423 4         10 my @unknown = grep { !$ann{ $_ } } keys( %$H );
  9         19  
3424 4         5 my $UE = $S->{unevaluatedProperties};
3425              
3426 4 50 33     9 if( !_is_true( $UE ) && !_is_hash( $UE ) )
    0          
3427             {
3428 4 100       10 if( @unknown )
3429             {
3430 1         24 _err_res( $ctx, $sp, "unevaluatedProperties not allowed: " . join( ',', @unknown ), 'unevaluatedProperties' );
3431 1         2 $ok = 0;
3432 1 50       2 return( { ok => 0, props => \%ann, items => {} } ) if( $bail_if_max->() );
3433             }
3434             }
3435             elsif( ref( $UE ) eq 'HASH' )
3436             {
3437 0         0 for my $k ( @unknown )
3438             {
3439 0         0 my $r = _v( $ctx, _join_ptr( $sp, "unevaluatedProperties" ), $UE, $H->{ $k } );
3440 0 0       0 if( !$r->{ok} )
3441             {
3442 0         0 $ok = 0;
3443 0 0       0 return( { ok => 0, props => \%ann, items => {} } ) if( $bail_if_max->() );
3444 0         0 next;
3445             }
3446 0         0 $ann{ $k } = 1;
3447             }
3448             }
3449             }
3450              
3451 285 100       2348 return( { ok => ( $ok ? 1 : 0 ), props => \%ann, items => {} } );
3452             }
3453              
3454             sub _k_string
3455             {
3456 50     50   114 my( $ctx, $s, $S, $ptr ) = @_;
3457 50         148 my $len = _strlen( $s );
3458              
3459 50 100 100     209 if( exists( $S->{minLength} ) && $len < $S->{minLength} )
3460             {
3461 7 100       31 _t( $ctx, $ptr, 'minLength', undef, 'fail', "len=$len < $S->{minLength}" ) if( $ctx->{trace_on} );
3462 7         49 return( _err( $ctx, $ptr, "string shorter than minLength $S->{minLength}", 'minLength' ) );
3463             }
3464 43 50 66     173 _t( $ctx, $ptr, 'minLength', undef, 'pass', "len=$len >= $S->{minLength}" ) if( exists( $S->{minLength} ) && $ctx->{trace_on} );
3465              
3466 43 100 100     156 if( exists( $S->{maxLength} ) && $len > $S->{maxLength} )
3467             {
3468 1 50       4 _t( $ctx, $ptr, 'maxLength', undef, 'fail', "len=$len > $S->{maxLength}" ) if( $ctx->{trace_on} );
3469 1         5 return( _err( $ctx, $ptr, "string longer than maxLength $S->{maxLength}", 'maxLength' ) );
3470             }
3471 42 50 66     89 _t( $ctx, $ptr, 'maxLength', undef, 'pass', "len=$len <= $S->{maxLength}" ) if( exists( $S->{maxLength} ) && $ctx->{trace_on} );
3472              
3473 42 100       104 if( exists( $S->{pattern} ) )
3474             {
3475 18         21 my $re = $S->{pattern};
3476 18         19 local $@;
3477 18         18 my $ok = eval{ $s =~ /$re/ };
  18         233  
3478 18 0       38 _t( $ctx, $ptr, 'pattern', undef, $ok ? 'pass' : 'fail', "/$re/" ) if( $ctx->{trace_on} );
    50          
3479 18 100       45 return( _err( $ctx, $ptr, "string does not match pattern /$re/", 'pattern' ) ) unless( $ok );
3480             }
3481 40         106 return(1);
3482             }
3483              
3484             # Primitive keyword helpers
3485             sub _k_type
3486             {
3487 582     582   1358 my( $ctx, $v, $type_kw, $ptr ) = @_;
3488              
3489 582 100       1555 my @alts = ref( $type_kw ) eq 'ARRAY' ? @$type_kw : ( $type_kw );
3490              
3491             # First, allow inline schemas in the union
3492 582         799 my $i = 0;
3493 582         886 for my $alt ( @alts )
3494             {
3495 642 100       1159 if( ref( $alt ) eq 'HASH' )
3496             {
3497 20         57 my $r = _v( $ctx, _join_ptr( $ptr, "type/$i" ), $alt, $v );
3498 20 100       116 return(1) if( $r->{ok} );
3499             }
3500 639         914 $i++;
3501             }
3502              
3503             # Then, try string type names
3504 579         837 for my $t ( @alts )
3505             {
3506 610 100       1061 next if( ref( $t ) );
3507 595 100       1174 return(1) if( _match_type( $v, $t ) );
3508             }
3509              
3510 36 100       62 my $exp = join( '|', map{ ref( $_ ) ? 'schema' : $_ } @alts );
  49         225  
3511 36         152 return( _err( $ctx, $ptr, "type mismatch: expected $exp", 'type' ) );
3512             }
3513              
3514             sub _k_unique_keys
3515             {
3516 17     17   29 my( $ctx, $sp, $uk_def, $array ) = @_;
3517 17 50 33     53 unless( ref( $uk_def ) eq 'ARRAY' && @$uk_def )
3518             {
3519 0         0 return( { ok => 1, props => {}, items => {} } );
3520             }
3521              
3522 17         27 for my $key_set ( @$uk_def )
3523             {
3524 19 50 33     54 next unless( ref( $key_set ) eq 'ARRAY' && @$key_set );
3525              
3526 19         25 my %seen;
3527 19         48 for my $i ( 0 .. $#$array )
3528             {
3529 42         67 my $item = $array->[$i];
3530 42 100       71 next unless( ref( $item ) eq 'HASH' );
3531              
3532 40         46 my @key_vals;
3533 40         39 my $all_present = 1;
3534 40         67 for my $key ( @$key_set )
3535             {
3536 47 100       96 if( exists( $item->{ $key } ) )
3537             {
3538 44         79 push( @key_vals, _canon( $item->{ $key } ) );
3539             }
3540             else
3541             {
3542 3         4 $all_present = 0;
3543 3         6 last;
3544             }
3545             }
3546              
3547             # Skip items that do not have *all* keys in this key set
3548 40 100       92 next unless( $all_present );
3549              
3550 37         65 my $composite = join( "\0", @key_vals );
3551 37 100       78 if( exists( $seen{ $composite } ) )
3552             {
3553 8         14 my $prev_i = $seen{ $composite };
3554 8         17 my $keys = join( ', ', map { "'$_'" } @$key_set );
  9         31  
3555 8         13 push( @{$ctx->{ptr_stack}}, _join_ptr( $sp, $i ) );
  8         24  
3556 8         36 my $res = _err_res(
3557             $ctx,
3558             $sp,
3559             "uniqueKeys violation: items[$prev_i] and items[$i] have identical values for key(s) [$keys]",
3560             'uniqueKeys',
3561             );
3562 8         11 pop( @{$ctx->{ptr_stack}} );
  8         19  
3563              
3564 8         36 return( $res );
3565             }
3566 29         104 $seen{ $composite } = $i;
3567             }
3568             }
3569              
3570 9         53 return( { ok => 1, props => {}, items => {} } );
3571             }
3572              
3573             sub _match_type
3574             {
3575 595     595   1052 my( $v, $t ) = @_;
3576              
3577 595 100 66     1347 return(1) if( $t eq 'null' && !defined( $v ) );
3578              
3579 594 100       1118 if( $t eq 'boolean' )
3580             {
3581 16 100       37 return(0) if( !defined( $v ) );
3582 14 100 66     79 if( blessed( $v ) && ( ref( $v ) =~ /Boolean/ ) )
3583             {
3584 8         88 my $s = "$v";
3585 8 50 66     95 return( ( $s eq '0' || $s eq '1' ) ? 1 : 0 );
3586             }
3587 6 100       62 return( ( $v =~ /\A(?:0|1|true|false)\z/i ) ? 1 : 0 );
3588             }
3589              
3590 578 100       1102 if( $t eq 'integer' )
3591             {
3592 113 100       292 return(0) unless( _is_number( $v ) );
3593 99 100       1187 return( ( $v =~ /\A-?(?:0|[1-9][0-9]*)\z/ ) ? 1 : 0 );
3594             }
3595              
3596 465 100       883 if( $t eq 'number' )
3597             {
3598 33         66 return( _is_number( $v ) );
3599             }
3600              
3601 432 100       832 if( $t eq 'string' )
3602             {
3603             # Must be a scalar, defined, and NOT a numeric SV under strict typing.
3604 140 100 100     707 return( (!ref( $v ) && defined( $v ) && !_is_number( $v )) ? 1 : 0 );
3605             }
3606              
3607 292 100 100     898 return(1) if( $t eq 'array' && ref( $v ) eq 'ARRAY' );
3608 255 100 100     1739 return(1) if( $t eq 'object' && ref( $v ) eq 'HASH' );
3609              
3610 19         33 return(0);
3611             }
3612              
3613             sub _normalize_uri
3614             {
3615 119     119   285 my( $u ) = @_;
3616 119 50 33     604 return( '#' ) unless( defined( $u ) && length( $u ) );
3617 119         282 return( $u );
3618             }
3619              
3620             sub _parse_media_type
3621             {
3622 7     7   10 my( $s ) = @_;
3623 7         28 my( $type, @rest ) = split( /;/, "$s" );
3624 7   50     13 $type ||= '';
3625 7         12 $type =~ s/\s+//g;
3626 7         17 my %p;
3627 7         11 for my $kv ( @rest )
3628             {
3629 7         16 my( $k, $v ) = split( /=/, $kv, 2 );
3630 7 50       14 next unless( defined( $k ) );
3631 7         34 $k =~ s/\s+//g;
3632 7 50       11 $v = '' unless( defined( $v ) );
3633 7         22 $v =~ s/^\s+|\s+$//g;
3634             # Allow single or double quote, but be consistent
3635 7         16 $v =~ s/^(?<quote>["'])(.*)\g{quote}$/$2/;
3636 7         30 $p{ lc( $k ) } = $v;
3637             }
3638 7         18 return( lc( $type ), \%p );
3639             }
3640              
3641             sub _prune_array_with_schema
3642             {
3643 1     1   3 my( $self, $schema, $data ) = @_;
3644              
3645 1         2 my @out;
3646              
3647 1         4 my $shape =
3648             {
3649             prefix_items => [], # index => subschema
3650             items => undef # subschema for additional items
3651             };
3652              
3653 1         6 $self->_extract_array_shape( $schema, $shape );
3654              
3655 1         3 for my $i ( 0 .. $#$data )
3656             {
3657 3         6 my $item = $data->[ $i ];
3658 3         5 my $item_schema;
3659              
3660 3 100       12 if( defined( $shape->{prefix_items}->[ $i ] ) )
    50          
3661             {
3662 1         3 $item_schema = $shape->{prefix_items}->[ $i ];
3663             }
3664             elsif( $shape->{items} )
3665             {
3666 2         3 $item_schema = $shape->{items};
3667             }
3668              
3669 3 100 66     13 if( $item_schema && ref( $item ) )
3670             {
3671 2         6 $out[ $i ] = $self->_prune_with_schema( $item_schema, $item );
3672             }
3673             else
3674             {
3675             # No structural knowledge or scalar item: keep as-is
3676 1         3 $out[ $i ] = $item;
3677             }
3678             }
3679              
3680 1         8 return( \@out );
3681             }
3682              
3683             sub _prune_object_with_schema
3684             {
3685 6     6   12 my( $self, $schema, $data ) = @_;
3686              
3687             # Collect effective object shape from this schema and any allOf mixins.
3688 6         39 my $shape =
3689             {
3690             props => {}, # property name => subschema
3691             patterns => [], # [ regex, subschema ] ...
3692             allow_ap => 1, # additionalProperties allowed?
3693             ap_schema => undef # subschema for additionalProperties, if any
3694             };
3695              
3696 6         24 $self->_extract_object_shape( $schema, $shape );
3697              
3698 6         10 my %clean;
3699              
3700             KEY:
3701 6         17 foreach my $key ( keys( %$data ) )
3702             {
3703 16         27 my $val = $data->{ $key };
3704              
3705             # 1) Direct properties
3706 16 100       35 if( exists( $shape->{props}->{ $key } ) )
3707             {
3708 7         12 my $sub = $shape->{props}->{ $key };
3709 7         21 $clean{ $key } = $self->_prune_with_schema( $sub, $val );
3710 7         20 next KEY;
3711             }
3712              
3713             # 2) patternProperties
3714 9         15 foreach my $pair ( @{$shape->{patterns}} )
  9         19  
3715             {
3716 3         8 my( $re, $pschema ) = @$pair;
3717 3         4 my $ok;
3718              
3719             {
3720 3         5 local $@;
  3         4  
3721 3 100       6 $ok = eval{ $key =~ /$re/ ? 1 : 0; };
  3         34  
3722             }
3723              
3724 3 100       10 next unless( $ok );
3725              
3726 2         6 $clean{ $key } = $self->_prune_with_schema( $pschema, $val );
3727 2         6 next KEY;
3728             }
3729              
3730             # 3) additionalProperties
3731 7 50       17 if( $shape->{allow_ap} )
3732             {
3733 0 0 0     0 if( $shape->{ap_schema} && ref( $val ) )
3734             {
3735 0         0 $clean{ $key } = $self->_prune_with_schema( $shape->{ap_schema}, $val );
3736             }
3737             else
3738             {
3739             # allowed, but no further structure known
3740 0         0 $clean{ $key } = $val;
3741             }
3742             }
3743             else
3744             {
3745             # additionalProperties is false (or equivalent) => drop unknown key
3746 7         14 next KEY;
3747             }
3748             }
3749              
3750 6         44 return( \%clean );
3751             }
3752              
3753             sub _prune_with_schema
3754             {
3755 17     17   40 my( $self, $schema, $data ) = @_;
3756              
3757             # Boolean schemas and non-hash schemas: do not attempt pruning.
3758 17 50       42 return( $data ) unless( ref( $schema ) eq 'HASH' );
3759              
3760             # Only prune structured values; scalars we leave untouched.
3761 17 100       43 if( ref( $data ) eq 'HASH' )
    100          
3762             {
3763 6         17 return( $self->_prune_object_with_schema( $schema, $data ) );
3764             }
3765             elsif( ref( $data ) eq 'ARRAY' )
3766             {
3767 1         7 return( $self->_prune_array_with_schema( $schema, $data ) );
3768             }
3769              
3770 10         60 return( $data );
3771             }
3772              
3773             sub _ptr_of_node
3774             {
3775 8     8   15 my( $root, $target ) = @_;
3776 8         11 for my $p ( keys( %{$root->{anchors}} ) )
  8         39  
3777             {
3778 74         225 my $n = $root->{anchors}->{ $p };
3779 74 100       319 return( $p ) if( $n eq $target );
3780             }
3781 0         0 return( '#' );
3782             }
3783              
3784             my %SCRIPT_LIKE = map{ $_ => 1 } qw(
3785             Hiragana Katakana Han Hangul Latin Cyrillic Greek
3786             Hebrew Thai Armenian Georgian
3787             Arabic Devanagari Bengali Gurmukhi Gujarati Oriya
3788             Tamil Telugu Kannada Malayalam Sinhala
3789             Lao Tibetan Myanmar Khmer
3790             );
3791             sub _re_to_js
3792             {
3793 0     0   0 my $re = shift( @_ );
3794 0         0 my %opt = @_;
3795              
3796             # style: 'literal' => for use in /.../u
3797             # 'string' => for use in new RegExp("...", "u")
3798 0   0     0 my $style = $opt{style} || 'string';
3799              
3800 0 0 0     0 return if( !defined( $re ) || !length( $re ) );
3801              
3802             #
3803             # 1) Convert \x{...} (Perl) to \u.... or \u{....} (JS)
3804             # - 1–4 hex digits -> \uHHHH
3805             # - 5–6 hex digits -> \u{HHHHH} (requires /u in JS)
3806             #
3807 0         0 $re =~ s{
3808             \\x\{([0-9A-Fa-f]{1,6})\}
3809             }{
3810 0         0 my $hex = uc( $1 );
3811 0 0       0 if( length( $hex ) <= 4 )
3812             {
3813 0         0 "\\u$hex";
3814             }
3815             else
3816             {
3817 0         0 "\\u\{$hex\}";
3818             }
3819             }egx;
3820              
3821             # Script eXtended
3822             #
3823             # 2) Convert \p{Katakana} / \P{Katakana}
3824             # to \p{scx=Katakana} / \P{scx=Katakana}
3825             # (Script_Extensions so it covers half-width too)
3826             #
3827 0         0 $re =~ s[
3828             \\([pP])\{([^}]+)\}
3829             ][
3830 0         0 my( $p, $name ) = ( $1, $2 );
3831              
3832 0         0 my $norm = $name;
3833 0         0 $norm =~ s/^\s+|\s+$//g;
3834 0         0 $norm = ucfirst( lc( $norm ) );
3835              
3836 0 0       0 if( exists( $SCRIPT_LIKE{ $norm } ) )
3837             {
3838 0         0 "\\$p\{scx=$norm\}";
3839             }
3840             else
3841             {
3842             # Pass other properties through unchanged:
3843             # \p{L}, \p{Letter}, \p{Nd}, etc.
3844 0         0 "\\$p\{$name\}";
3845             }
3846             ]egx;
3847              
3848             #
3849             # 3) Optionally escape backslashes for JS string literal
3850             #
3851 0 0       0 if( $style eq 'string' )
3852             {
3853 0         0 $re =~ s{\\}{\\\\}g; # existing
3854 0         0 $re =~ s{"}{\\"}g; # escape "
3855 0         0 $re =~ s{\n}{\\n}g;
3856 0         0 $re =~ s{\r}{\\r}g;
3857 0         0 $re =~ s{/}{\/}g;
3858             }
3859              
3860             # For 'literal', we leave backslashes as-is, for use in /.../u
3861              
3862 0         0 return( $re );
3863             }
3864              
3865             sub _register_builtin_media_validators
3866             {
3867 4     4   13 my( $self ) = @_;
3868              
3869             # Example: application/json
3870             $self->register_media_validator( 'application/json' => sub
3871             {
3872 2     2   5 my( $bytes, $params ) = @_;
3873 2         3 local $@;
3874 2         2 my $v = eval{ JSON->new->allow_nonref(1)->decode( $bytes ) };
  2         31  
3875 2 50       5 return( 0, 'invalid JSON', undef ) if( $@ );
3876             # JSON value is valid even if it’s 0, "", or false
3877 2         6 return( 1, undef, $v );
3878 4         42 } );
3879              
3880 4         7 return( $self );
3881             }
3882              
3883             sub _resolve_uri
3884             {
3885 37     37   67 my( $base, $ref ) = @_;
3886 37 100 33     414 return( $ref ) if( !defined( $base ) || $base eq '' || $ref =~ /^[A-Za-z][A-Za-z0-9+\-.]*:/ );
      66        
3887 18 100       57 if( $ref =~ /^\#/ )
3888             {
3889 17         33 ( my $no_frag = $base ) =~ s/\#.*$//;
3890 17         51 return( $no_frag . $ref );
3891             }
3892 1 50       4 return( $base . $ref ) if( $base =~ /\/$/ );
3893 1         15 ( my $dir = $base ) =~ s{[^/]*$}{};
3894 1         4 return( $dir . $ref );
3895             }
3896              
3897             sub _safe_invoke
3898             {
3899 8     8   18 my( $cb, @args ) = @_;
3900 8         7 local $@;
3901             # Force list context to preserve (ok, msg, out) style returns
3902 8         14 my @ret = eval{ $cb->( @args ) };
  8         17  
3903 8 50 0     70 return( 0, ( $@ || 'callback failed' ), undef ) if( $@ );
3904              
3905             # If callback returns (ok, msg, out) or (ok, msg)
3906 8 50       13 if( @ret >= 2 )
3907             {
3908 8 100       19 my( $ok, $msg, $out ) = ( $ret[0] ? 1 : 0, $ret[1], $ret[2] );
3909 8         45 return( $ok, $msg, $out );
3910             }
3911              
3912             # If callback returns a single value
3913 0 0       0 if( @ret == 1 )
3914             {
3915 0         0 my $v = $ret[0];
3916             # Reference => treat as decoded structure (success)
3917 0 0       0 return( 1, undef, $v ) if( ref( $v ) );
3918             # Defined scalar => truthiness decides; scalar can be treated as decoded bytes
3919 0 0       0 return( $v ? 1 : 0, undef, ( defined( $v ) ? $v : undef ) );
    0          
3920             }
3921              
3922             # Empty list => treat as failure (safer default)
3923 0         0 return( 0, 'callback returned no value', undef );
3924             }
3925              
3926             sub _strlen
3927             {
3928 50     50   87 my( $s ) = @_;
3929 50 50       559 $s = Encode::decode( 'UTF-8', "$s", Encode::FB_DEFAULT ) unless( Encode::is_utf8( $s ) );
3930 50         2280 my @cp = unpack( 'U*', $s );
3931 50         124 return( scalar( @cp ) );
3932             }
3933              
3934             # Strict base64: validates alphabet, padding, length, and round-trips
3935             sub _strict_base64_decode
3936             {
3937 6     6   12 my( $s ) = @_;
3938 6 50       13 return unless( defined( $s ) );
3939              
3940             # strip ASCII whitespace per RFC 4648 §3.3 (tests commonly include raw)
3941 6         99 ( my $norm = "$s" ) =~ s/\s+//g;
3942              
3943             # valid alphabet + padding only
3944 6 100       72 return unless( $norm =~ /\A[A-Za-z0-9+\/]*={0,2}\z/ );
3945              
3946             # length must be a multiple of 4
3947 3 50       8 return unless( ( length( $norm ) % 4 ) == 0 );
3948              
3949 3         38 local $@;
3950 3 50       4 return unless( eval{ require MIME::Base64; 1 } );
  3         25  
  3         6  
3951              
3952 3         14 my $out = MIME::Base64::decode_base64( $norm );
3953              
3954             # re-encode and compare to ensure no silent salvage
3955 3         14 my $re = MIME::Base64::encode_base64( $out, '' );
3956             # RFC allows omitting trailing '=' if not needed; normalize both
3957 3         7 $re =~ s/\s+//g;
3958 3         4 $norm =~ s/\s+//g;
3959 3 50       8 return unless( $re eq $norm );
3960 3         6 return( $out );
3961             }
3962              
3963             sub _t
3964             {
3965 44     44   113 my( $ctx, $schema_ptr, $keyword, $inst_path, $outcome, $note ) = @_;
3966 44 50       98 return unless( $ctx->{trace_on} );
3967              
3968 44 50 33     102 if( $ctx->{trace_limit} && @{$ctx->{trace}} >= $ctx->{trace_limit} )
  44         174  
3969             {
3970 0         0 return;
3971             }
3972 44 100       106 if( $ctx->{trace_sample} )
3973             {
3974 8 50       74 return if( int( rand(100) ) >= $ctx->{trace_sample} );
3975             }
3976              
3977 36         266 push( @{$ctx->{trace}},
3978             {
3979             schema_ptr => $schema_ptr,
3980             keyword => $keyword,
3981 36   50     55 inst_path => ( $ctx->{ptr_stack}->[-1] // '#' ),
3982             outcome => $outcome, # 'pass' | 'fail'
3983             note => $note, # short string
3984             });
3985             }
3986              
3987             # Validation core with annotation + recursion
3988             # _v returns { ok => 0|1, props => {k=>1,...}, items => {i=>1,...} }
3989             sub _v
3990             {
3991 715     715   1502 my( $ctx, $schema_ptr, $schema, $inst ) = @_;
3992              
3993             # Recursion guard only for reference types
3994 715 100       1421 if( ref( $inst ) )
3995             {
3996 364         877 my $inst_addr = _inst_addr( $inst, $ctx->{ptr_stack}->[-1] );
3997 364         665 my $vkey = "$schema_ptr|$inst_addr";
3998 364 50       1388 return( { ok => 1, props => {}, items => {} } ) if( $ctx->{visited}->{ $vkey }++ );
3999             }
4000              
4001             # Enter dynamicAnchor scope if present
4002 715         975 my $frame_added = 0;
4003 715 50 66     2936 if( ref( $schema ) eq 'HASH' &&
      66        
      66        
4004             exists( $schema->{'$dynamicAnchor'} ) &&
4005             defined( $schema->{'$dynamicAnchor'} ) &&
4006             $schema->{'$dynamicAnchor'} ne '' )
4007             {
4008 11         15 my %frame = %{$ctx->{dyn_stack}->[-1]}; # inherit
  11         60  
4009 11         26 $frame{ $schema->{'$dynamicAnchor'} } = $schema;
4010 11         17 push( @{$ctx->{dyn_stack}}, \%frame );
  11         27  
4011 11         25 $frame_added = 1;
4012             }
4013              
4014 715         2386 my $res = _v_node( $ctx, $schema_ptr, $schema, $inst );
4015              
4016 715 100       1325 if( $frame_added )
4017             {
4018 11         14 pop( @{$ctx->{dyn_stack}} );
  11         50  
4019             }
4020              
4021 715         1311 return( $res );
4022             }
4023              
4024             sub _v_node
4025             {
4026 715     715   1270 my( $ctx, $schema_ptr, $schema, $inst ) = @_;
4027              
4028             # $ref / $dynamicRef first
4029 715 100 66     2370 if( ref( $schema ) eq 'HASH' &&
4030             exists( $schema->{'$ref'} ) )
4031             {
4032 22         75 return( _apply_ref( $ctx, $schema_ptr, $schema->{'$ref'}, $inst ) );
4033             }
4034 693 100 66     9330 if( ref( $schema ) eq 'HASH' &&
4035             exists( $schema->{'$dynamicRef'} ) )
4036             {
4037 5         41 return( _apply_dynamic_ref( $ctx, $schema_ptr, $schema->{'$dynamicRef'}, $inst ) );
4038             }
4039 688 100 66     2457 if( ref( $schema ) eq 'HASH' &&
      66        
4040             exists( $schema->{'$comment'} ) &&
4041             defined( $schema->{'$comment'} ) )
4042             {
4043 9         17 my $c = $schema->{'$comment'};
4044 9 100       27 if( my $cb = $ctx->{comment_handler} )
4045             {
4046 4         8 local $@;
4047 4         10 eval{ $cb->( $schema_ptr, "$c" ) };
  4         21  
4048             # ignore callback errors to keep validation resilient
4049             }
4050 9 50       70 _t( $ctx, $schema_ptr, '$comment', undef, 'visit', "$c" ) if( $ctx->{trace_on} );
4051             }
4052              
4053 688 100       1458 _t( $ctx, $schema_ptr, 'node', undef, 'visit' ) if( $ctx->{trace_on} );
4054              
4055             # Use compiled validator if enabled
4056 688 100       2144 if( $ctx->{compile_on} )
4057             {
4058 35         73 my $fn = $ctx->{root}->{fn_index}->{ $schema_ptr };
4059 35 100       68 unless( $fn )
4060             {
4061 23         63 $fn = _compile_node( $ctx->{root}, $schema_ptr, $schema );
4062 23         97 $ctx->{root}->{fn_index}->{ $schema_ptr } = $fn;
4063             }
4064 35         95 return( $fn->( $ctx, $inst ) );
4065             }
4066              
4067 653 50       1337 return( { ok => 1, props => {}, items => {} } ) unless( ref( $schema ) eq 'HASH' );
4068              
4069 653         971 my $ptr = $schema_ptr;
4070              
4071             # Types / const / enum
4072 653 100       1312 if( exists( $schema->{type} ) )
4073             {
4074 547 100       1418 _k_type( $ctx, $inst, $schema->{type}, $ptr ) or return( _fail() );
4075             }
4076 617 100       1392 if( exists( $schema->{const} ) )
4077             {
4078 6 50       15 _k_const( $ctx, $inst, $schema->{const}, $ptr ) or return( _fail() );
4079             }
4080 617 100       1117 if( exists( $schema->{enum} ) )
4081             {
4082 8 100       18 _k_enum( $ctx, $inst, $schema->{enum}, $ptr ) or return( _fail() );
4083             }
4084 613 50       1139 _t( $ctx, $schema_ptr, 'type/const/enum', undef, 'pass', '' ) if( $ctx->{trace_on} );
4085              
4086 613 50 100     1322 if( $ctx->{unique_keys} &&
      66        
      66        
4087             exists( $schema->{uniqueKeys} ) &&
4088             ref( $schema->{uniqueKeys} ) eq 'ARRAY' &&
4089             ref( $inst ) eq 'ARRAY' )
4090             {
4091 17         32 my $r = _k_unique_keys( $ctx, $ptr, $schema->{uniqueKeys}, $inst );
4092 17 100       70 return( $r ) unless( $r->{ok} );
4093             }
4094              
4095             # Numbers
4096 605 100       1173 if( _is_number( $inst ) )
4097             {
4098 139         352 for my $k ( qw( multipleOf minimum maximum exclusiveMinimum exclusiveMaximum ) )
4099             {
4100 631 100       1252 next unless( exists( $schema->{ $k } ) );
4101 86 100       251 _k_number( $ctx, $inst, $k, $schema->{ $k }, $ptr ) or return( _fail() );
4102             }
4103             }
4104              
4105             # Strings
4106 583 100 100     1710 if( !ref( $inst ) && defined( $inst ) )
4107             {
4108 265 100 66     2575 if( exists( $schema->{minLength} ) || exists( $schema->{maxLength} ) || exists( $schema->{pattern} ) )
      66        
4109             {
4110 48 100       107 _k_string( $ctx, $inst, $schema, $ptr ) or return( _fail() );
4111             }
4112 256 100       545 if( exists( $schema->{format} ) )
4113             {
4114 53 100       169 _k_format( $ctx, $inst, $schema->{format}, $ptr ) or return( _fail() );
4115             }
4116              
4117             # contentEncoding / contentMediaType / contentSchema
4118 239 50 66     1323 if( exists( $schema->{contentEncoding} ) ||
      33        
4119             exists( $schema->{contentMediaType} ) ||
4120             exists( $schema->{contentSchema} ) )
4121             {
4122 9 100       18 my $assert = $ctx->{content_assert} ? 1 : 0;
4123 9         15 my $bytes = "$inst";
4124 9         13 my $decoded_ref;
4125              
4126 9 50       15 if( exists( $schema->{contentEncoding} ) )
4127             {
4128 9         32 my $dec = _content_decode( $ctx, $schema->{contentEncoding}, $bytes );
4129 9 100       23 if( !defined( $dec ) )
4130             {
4131 3 100       19 return( _err_res( $ctx, $ptr, "contentEncoding '$schema->{contentEncoding}' decode failed", 'contentEncoding' ) ) if( $assert );
4132             }
4133             else
4134             {
4135 6         8 $bytes = $dec;
4136             }
4137             }
4138              
4139 7 50       14 if( exists( $schema->{contentMediaType} ) )
4140             {
4141 7         21 my( $mt, $params ) = _parse_media_type( $schema->{contentMediaType} );
4142 7 100       22 if( my $cb = $ctx->{media_validators}->{ $mt } )
4143             {
4144 5         11 my( $ok, $msg, $maybe_decoded ) = _safe_invoke( $cb, $bytes, $params );
4145 5 100       13 if( !$ok )
4146             {
4147 2 100 33     13 return( _err_res( $ctx, $ptr, ( $msg || "contentMediaType '$mt' validation failed", 'contentMediaType' ) ) ) if( $assert );
4148             }
4149 4 100       12 $decoded_ref = $maybe_decoded if( ref( $maybe_decoded ) );
4150 4 100 100     22 $bytes = $maybe_decoded if( defined( $maybe_decoded ) && !ref( $maybe_decoded ) );
4151             }
4152             else
4153             {
4154 2 50 0     7 if( $mt =~ m{\Atext/} && ( ( $params->{charset} || '' ) =~ /\Autf-?8\z/i ) )
      33        
4155             {
4156 0         0 local $@;
4157             my $ok = eval
4158 0 0       0 {
4159 0         0 require Encode;
4160 0         0 Encode::decode( 'UTF-8', $bytes, Encode::FB_CROAK );
4161 0         0 1;
4162             } ? 1 : 0;
4163 0 0 0     0 if( !$ok && $assert )
4164             {
4165 0         0 return( _err_res( $ctx, $ptr, "contentMediaType '$mt' invalid UTF-8", 'contentMediaType' ) );
4166             }
4167             }
4168             }
4169             }
4170              
4171 6 100       15 if( exists( $schema->{contentSchema} ) )
4172             {
4173 4         8 my $val;
4174 4 100       24 if( ref( $decoded_ref ) )
4175             {
4176 2         3 $val = $decoded_ref; # already decoded by media validator
4177             }
4178             else
4179             {
4180 2         3 local $@;
4181 2         2 $val = eval{ JSON->new->allow_nonref(1)->utf8(1)->decode( $bytes ) };
  2         59  
4182             }
4183              
4184 4 100       12 if( !defined( $val ) )
4185             {
4186 1 50       3 return( _err_res( $ctx, $ptr, "contentSchema present but payload not JSON-decodable", 'contentSchema' ) ) if( $assert );
4187             }
4188             else
4189             {
4190 3         9 my $r = _v( $ctx, _join_ptr( $ptr, 'contentSchema' ), $schema->{contentSchema}, $val );
4191 3 100       12 return( $r ) unless( $r->{ok} );
4192             }
4193             }
4194             }
4195             }
4196              
4197 553         762 my %ann_props;
4198             my %ann_items;
4199              
4200             # Arrays
4201 553 100       1700 if( ref( $inst ) eq 'ARRAY' )
4202             {
4203 32         119 my $r = _k_array_all( $ctx, $schema_ptr, $schema, $inst );
4204 32 100       107 return( $r ) unless( $r->{ok} );
4205 23         42 %ann_items = ( %ann_items, %{ $r->{items} } );
  23         115  
4206             }
4207 544 50 66     1227 _t( $ctx, $schema_ptr, 'array', undef, 'pass', '' ) if( ref( $inst ) eq 'ARRAY' && $ctx->{trace_on} );
4208              
4209             # Objects
4210 544 100       975 if( ref( $inst ) eq 'HASH' )
4211             {
4212 270         887 my $r = _k_object_all( $ctx, $schema_ptr, $schema, $inst );
4213 270 100       760 return( $r ) unless( $r->{ok} );
4214 157         309 %ann_props = ( %ann_props, %{ $r->{props} } );
  157         618  
4215             }
4216 431 50 66     1311 _t( $ctx, $schema_ptr, 'object', undef, 'pass', '' ) if( ref( $inst ) eq 'HASH' && $ctx->{trace_on} );
4217              
4218             # Combinators
4219 431         650 for my $comb ( qw( allOf anyOf oneOf not ) )
4220             {
4221 1706 100       2850 next unless( exists( $schema->{ $comb } ) );
4222 25         90 my $r = _k_combinator( $ctx, $schema_ptr, $schema, $inst, $comb );
4223 25 100       65 return( $r ) unless( $r->{ok} );
4224 13         30 %ann_props = ( %ann_props, %{ $r->{props} } );
  13         27  
4225 13         19 %ann_items = ( %ann_items, %{ $r->{items} } );
  13         16  
4226 13 50       38 _t( $ctx, $schema_ptr, $comb, undef, 'pass', '' ) if( $ctx->{trace_on} );
4227             }
4228              
4229             # Conditionals
4230 419 100       860 if( exists( $schema->{if} ) )
4231             {
4232 16         29 my $r = _k_if_then_else( $ctx, $schema_ptr, $schema, $inst );
4233 16 100       30 return( $r ) unless( $r->{ok} );
4234 13         18 %ann_props = ( %ann_props, %{ $r->{props} } );
  13         17  
4235 13         19 %ann_items = ( %ann_items, %{ $r->{items} } );
  13         48  
4236             }
4237              
4238 416 50       884 _t( $ctx, $schema_ptr, 'node', undef, 'pass' ) if( $ctx->{trace_on} );
4239 416         1819 return( { ok => 1, props => \%ann_props, items => \%ann_items } );
4240             }
4241              
4242             # NOTE: JSON::Schema::Validate::Error
4243             package JSON::Schema::Validate::Error;
4244             BEGIN
4245             {
4246 44     44   754 use strict;
  44         138  
  44         2287  
4247 44     44   419 use warnings;
  44         204  
  44         3554  
4248 44     44   320 use vars qw( $VERSION );
  44         124  
  44         7412  
4249             use overload (
4250             '""' => 'as_string',
4251 2     2   47 'eq' => sub{ _obj_eq(@_) },
4252 3     3   11 'ne' => sub{ !_obj_eq(@_) },
4253 1     1   5 '==' => sub{ _obj_eq(@_) },
4254 0     0   0 '!=' => sub{ !_obj_eq(@_) },
4255 5     5   239 bool => sub{1},
4256 44         850 fallback => 1,
4257 44     44   286 );
  44         111  
4258 44     44   8546 our $VERSION = 'v0.1.0';
4259             };
4260              
4261 44     44   220 use strict;
  44         73  
  44         1085  
4262 44     44   204 use warnings;
  44         126  
  44         2269  
4263 44     44   991 use utf8;
  44         426  
  44         398  
4264              
4265             sub new
4266             {
4267 180     180   234524 my $that = shift( @_ );
4268 180         432 my $ref = {};
4269             # Legacy instantiation
4270             # We make sure this is not one single option that was provided to us.
4271 180 100 66     592 if( @_ == 2 && $_[0] !~ /^(?:path|message|keyword|schema_pointer)$/ )
4272             {
4273 4         15 @$ref{qw( path message )} = @_;
4274             }
4275             else
4276             {
4277 176         957 my $args = { @_ };
4278 176         430 for( qw( path message keyword schema_pointer ) )
4279             {
4280 704 50       2028 $ref->{ $_ } = $args->{ $_ } if( exists( $args->{ $_ } ) );
4281             }
4282             }
4283 180   33     999 return( bless( $ref => ( ref( $that ) || $that ) ) );
4284             }
4285              
4286             sub as_hash
4287             {
4288 0     0   0 my $self = shift( @_ );
4289 0         0 my $ref = {};
4290 0         0 my @keys = qw( keyword message path schema_pointer );
4291 0         0 @$ref{ @keys } = @$self{ @keys };
4292 0         0 return( $ref );
4293             }
4294              
4295             sub as_string
4296             {
4297 9     9   26 my $self = shift( @_ );
4298 9   100     34 my $sp = $self->schema_pointer // '';
4299 9   50     38 my $path = $self->path // '';
4300 9   50     31 my $msg = $self->message // '';
4301             # Dual-path if avail
4302 9 100       147 return( $sp ? "${sp} → ${path}: ${msg}" : "${path}: ${msg}" );
4303             }
4304              
4305             sub keyword
4306             {
4307 8     8   3151 my $self = shift( @_ );
4308 8 50       29 $self->{keyword} = shift( @_ ) if( @_ );
4309 8         81 return( $self->{keyword} );
4310             }
4311              
4312             sub message
4313             {
4314 29     29   73 my $self = shift( @_ );
4315 29 50       73 $self->{message} = shift( @_ ) if( @_ );
4316 29         206 return( $self->{message} );
4317             }
4318              
4319             sub path
4320             {
4321 17     17   1103 my $self = shift( @_ );
4322 17 50       44 $self->{path} = shift( @_ ) if( @_ );
4323 17         67 return( $self->{path} );
4324             }
4325              
4326             sub schema_pointer
4327             {
4328 10     10   22 my $self = shift( @_ );
4329 10 50       82 $self->{schema_pointer} = shift( @_ ) if( @_ );
4330 10         114 return( $self->{schema_pointer} );
4331             }
4332              
4333             sub _obj_eq
4334             {
4335 44     44   26664 no overloading;
  44         120  
  44         10594  
4336 6     6   13 my $self = shift( @_ );
4337 6         10 my $other = shift( @_ );
4338 6         10 my $me;
4339 6 100 66     75 if( defined( $other ) &&
    50 66        
4340             Scalar::Util::blessed( $other ) &&
4341             $other->isa( 'JSON::Schema::Validate::Error' ) )
4342             {
4343 4 100 50     14 if( ( $self->message // '' ) eq ( $other->message // '' ) &&
      50        
      50        
      50        
      100        
4344             ( $self->path // '' ) eq ( $other->path // '' ) )
4345             {
4346 2         26 return(1);
4347             }
4348             else
4349             {
4350 2         13 return(0);
4351             }
4352             }
4353             # Compare error message
4354             elsif( !ref( $other ) )
4355             {
4356 2   50     6 my $me = $self->message // '';
4357 2         13 return( $me eq $other );
4358             }
4359             # Otherwise some reference data to which we cannot compare
4360 0           return(0) ;
4361             }
4362              
4363             1;
4364             # NOTE: POD
4365             __END__
4366              
4367             =encoding utf-8
4368              
4369             =head1 NAME
4370              
4371             JSON::Schema::Validate - Lean, recursion-safe JSON Schema validator (Draft 2020-12)
4372              
4373             =head1 SYNOPSIS
4374              
4375             use JSON::Schema::Validate;
4376             use JSON ();
4377              
4378             my $schema = {
4379             '$schema' => 'https://json-schema.org/draft/2020-12/schema',
4380             '$id' => 'https://example.org/s/root.json',
4381             type => 'object',
4382             required => [ 'name' ],
4383             properties => {
4384             name => { type => 'string', minLength => 1 },
4385             next => { '$dynamicRef' => '#Node' },
4386             },
4387             '$dynamicAnchor' => 'Node',
4388             additionalProperties => JSON::false,
4389             };
4390              
4391             my $js = JSON::Schema::Validate->new( $schema )
4392             ->compile
4393             ->content_checks
4394             ->ignore_unknown_required_vocab
4395             ->prune_unknown
4396             ->register_builtin_formats
4397             ->trace
4398             ->trace_limit(200) # 0 means unlimited
4399             ->unique_keys; # enable uniqueKeys
4400              
4401             You could also do:
4402              
4403             my $js = JSON::Schema::Validate->new( $schema,
4404             compile => 1,
4405             content_checks => 1,
4406             ignore_req_vocab => 1,
4407             prune_unknown => 1,
4408             trace_on => 1,
4409             trace_limit => 200,
4410             unique_keys => 1,
4411             )->register_builtin_formats;
4412              
4413             my $ok = $js->validate({ name => 'head', next => { name => 'tail' } })
4414             or die( $js->error );
4415              
4416             print "ok\n";
4417              
4418             # Override instance options for one call only (backward compatible)
4419             $js->validate( $data, max_errors => 1 )
4420             or die( $js->error );
4421              
4422             # Quick boolean check (records at most one error by default)
4423             $js->is_valid({ name => 'head', next => { name => 'tail' } })
4424             or die( $js->error );
4425              
4426             Generating a browser-side validator with L</compile_js>:
4427              
4428             use JSON::Schema::Validate;
4429             use JSON ();
4430              
4431             my $schema = JSON->new->decode( do {
4432             local $/;
4433             <DATA>;
4434             } );
4435              
4436             my $js = JSON::Schema::Validate->new( $schema )
4437             ->compile;
4438             my $ok = $js->validate({ name => 'head', next => { name => 'tail' } })
4439             or die( $js->error );
4440              
4441             # Generate a standalone JavaScript validator for use in a web page.
4442             # ecma => 2018 enables Unicode regex features when available.
4443             my $js_code = $validator->compile_js( ecma => 2018 );
4444              
4445             open my $fh, '>:encoding(UTF-8)', 'htdocs/js/schema-validator.js'
4446             or die( "Unable to write schema-validator.js: $!" );
4447             print {$fh} $js_code;
4448             close $fh;
4449              
4450             In your HTML:
4451              
4452             <script src="/js/schema-validator.js"></script>
4453             <script>
4454             function validateForm()
4455             {
4456             var src = document.getElementById('payload').value;
4457             var out = document.getElementById('errors');
4458             var inst;
4459              
4460             try
4461             {
4462             inst = JSON.parse( src );
4463             }
4464             catch( e )
4465             {
4466             out.textContent = "Invalid JSON: " + e;
4467             return;
4468             }
4469              
4470             // The generated file defines a global function `validate(inst)`
4471             // that returns an array of error objects.
4472             var errors = validate( inst );
4473              
4474             if( !errors || !errors.length )
4475             {
4476             out.textContent = "OK – no client-side schema errors.";
4477             return;
4478             }
4479              
4480             var lines = [];
4481             for( var i = 0; i < errors.length; i++ )
4482             {
4483             var e = errors[i];
4484             lines.push(
4485             e.path + " [" + e.keyword + "]: " + e.message
4486             );
4487             }
4488             out.textContent = lines.join("\n");
4489             }
4490             </script>
4491              
4492             See also the live demonstration on CodePen: L<https://codepen.io/jdeguest/pen/vEGjNYX>
4493              
4494             and the original announcement on Reddit: L<https://www.reddit.com/r/perl/comments/1p80dne/showcase_localised_json_schema_validation_in_perl/>
4495              
4496             =head1 VERSION
4497              
4498             v0.9.0
4499              
4500             =head1 DESCRIPTION
4501              
4502             C<JSON::Schema::Validate> is a compact, dependency-light validator for L<JSON Schema|https://json-schema.org/> draft 2020-12. It focuses on:
4503              
4504             =over 4
4505              
4506             =item *
4507              
4508             Correctness and recursion safety (supports C<$ref>, C<$dynamicRef>, C<$anchor>, C<$dynamicAnchor>).
4509              
4510             =item *
4511              
4512             Draft 2020-12 evaluation semantics, including C<unevaluatedItems> and C<unevaluatedProperties> with annotation tracking.
4513              
4514             =item *
4515              
4516             A practical Perl API (constructor takes the schema; call C<validate> with your data; inspect C<error> / C<errors> on failure).
4517              
4518             =item *
4519              
4520             Builtin validators for common C<format>s (date, time, email, hostname, ip, uri, uuid, JSON Pointer, etc.), with the option to register or override custom format handlers.
4521              
4522             =item *
4523              
4524             Optional code generation via L</compile_js> to run a subset of the schema client-side in JavaScript, using the same error structure as the Perl validator.
4525              
4526             =back
4527              
4528             This module is intentionally minimal compared to large reference implementations, but it implements the parts most people rely on in production.
4529              
4530             =head2 Supported Keywords (2020-12)
4531              
4532             =over 4
4533              
4534             =item * Types
4535              
4536             C<type> (string or array of strings), including union types. Unions may also include inline schemas (e.g. C<< type => [ 'integer', { minimum => 0 } ] >>).
4537              
4538             =item * Constant / Enumerations
4539              
4540             C<const>, C<enum>.
4541              
4542             =item * Numbers
4543              
4544             C<multipleOf>, C<minimum>, C<maximum>, C<exclusiveMinimum>, C<exclusiveMaximum>.
4545              
4546             =item * Strings
4547              
4548             C<minLength>, C<maxLength>, C<pattern>, C<format>.
4549              
4550             =item * Arrays
4551              
4552             C<prefixItems>, C<items>, C<contains>, C<minContains>, C<maxContains>, C<uniqueItems>, C<unevaluatedItems>.
4553              
4554             =item * Objects
4555              
4556             C<properties>, C<patternProperties>, C<additionalProperties>, C<propertyNames>, C<required>, C<dependentRequired>, C<dependentSchemas>, C<unevaluatedProperties>.
4557              
4558             =item * Combinators
4559              
4560             C<allOf>, C<anyOf>, C<oneOf>, C<not>.
4561              
4562             =item * Conditionals
4563              
4564             C<if>, C<then>, C<else>.
4565              
4566             =item * Referencing
4567              
4568             C<$id>, C<$anchor>, C<$ref>, C<$dynamicAnchor>, C<$dynamicRef>.
4569              
4570             =back
4571              
4572             The Perl engine supports the full list above. The generated JavaScript currently implements a pragmatic subset; see L</compile_js> for details.
4573              
4574             =head2 Formats
4575              
4576             Call C<register_builtin_formats> to install default validators for the following C<format> names:
4577              
4578             =over 4
4579              
4580             =item * C<date-time>, C<date>, C<time>, C<duration>
4581              
4582             For C<date-time> and C<date>, leverages L<DateTime::Format::ISO8601> when available, then falls back to L<Time::Piece> (core since perl 5.10), then to L<DateTime> if installed, and finally to strict regex checks. C<duration> is validated entirely by a strict ISO 8601 regex; no external module is required or used.
4583              
4584             =item * C<email>, C<idn-email>
4585              
4586             Imported and use the very complex and complete regular expression from L<Regexp::Common::Email::Address>, but without requiring this module.
4587              
4588             =item * C<hostname>, C<idn-hostname>
4589              
4590             C<idn-hostname> uses L<Net::IDN::Encode> if available; otherwise, applies a permissive Unicode label check and then C<hostname> rules.
4591              
4592             =item * C<ipv4>, C<ipv6>
4593              
4594             Strict regex-based validation.
4595              
4596             =item * C<uri>, C<uri-reference>, C<iri>
4597              
4598             Reasonable regex checks: C<uri> and C<iri> require a proper RFC-compliant scheme (C<ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )> followed by C<:>); C<iri> additionally permits Unicode characters in the path and query components. C<uri-reference> allows an optional scheme. None of these is a full RFC 2396/3987 parser.
4599              
4600             =item * C<uuid>
4601              
4602             Hyphenated 8-4-4-4-12 hex.
4603              
4604             =item * C<json-pointer>, C<relative-json-pointer>
4605              
4606             Conformant to RFC 6901 and the relative variant used by JSON Schema.
4607              
4608             =item * C<regex>
4609              
4610             Checks that the pattern compiles in Perl.
4611              
4612             =back
4613              
4614             Custom formats can be registered or override builtins via C<register_format> or the C<format =E<gt> { ... }> constructor option (see L</METHODS>).
4615              
4616             =head1 CONSTRUCTOR
4617              
4618             =head2 new
4619              
4620             my $js = JSON::Schema::Validate->new( $schema, %opts );
4621              
4622             Build a validator from a decoded JSON Schema (Perl hash/array structure), and returns the newly instantiated object.
4623              
4624             Options (all optional):
4625              
4626             =over 4
4627              
4628             =item C<compile =E<gt> 1|0>
4629              
4630             Defaults to C<0>
4631              
4632             Enable or disable the compiled-validator fast path.
4633              
4634             When enabled and the root has not been compiled yet, this triggers an initial compilation.
4635              
4636             =item C<content_assert =E<gt> 1|0>
4637              
4638             Defaults to C<0>
4639              
4640             Enable or disable the content assertions for the C<contentEncoding>, C<contentMediaType> and C<contentSchema> trio.
4641              
4642             When enabling, built-in media validators are registered (e.g. C<application/json>).
4643              
4644             =item C<extensions =E<gt> 1|0>
4645              
4646             Defaults to C<0>
4647              
4648             This enables or disables all non-core extensions currently implemented by the validator.
4649              
4650             When set to a true value, this enables the C<uniqueKeys> applicator. Future extensions (e.g. custom keywords, additional vocabularies) will also be controlled by this flag.
4651              
4652             When set to a true value, all known extensions are activated; setting it to false disables them all.
4653              
4654             If you set separately an extension boolean value, it will not be overriden by this. So for example:
4655              
4656             my $js = JSON::Schema::Validate->new( $schema, extension => 0, unique_keys => 1 );
4657              
4658             Will globally disable extension, but will enable C<uniqueKeys>
4659              
4660             Enabling extensions does not affect core Draft 2020-12 compliance — unknown keywords are still ignored unless explicitly supported.
4661              
4662             =item C<format =E<gt> \%callbacks>
4663              
4664             Hash of C<format_name =E<gt> sub{ ... }> validators. Each sub receives the string to validate and must return true/false. Entries here take precedence when you later call C<register_builtin_formats> (i.e. your callbacks remain in place).
4665              
4666             =item C<ignore_unknown_required_vocab =E<gt> 1|0>
4667              
4668             Defaults to C<0>
4669              
4670             If enabled, required vocabularies declared in C<$vocabulary> that are not advertised as supported by the caller will be I<ignored> instead of causing the validator to C<die>.
4671              
4672             You can also use C<ignore_req_vocab> for short.
4673              
4674             =item C<max_errors>
4675              
4676             Defaults to C<200>
4677              
4678             Sets the maximum number of errors to be recorded.
4679              
4680             =item C<normalize_instance =E<gt> 1|0>
4681              
4682             Defaults to C<1>
4683              
4684             When true, the instance is round-tripped through L<JSON> before validation, which enforces strict JSON typing (strings remain strings; numbers remain numbers). This matches Python C<jsonschema>’s type behaviour. Set to C<0> if you prefer Perl’s permissive numeric/string duality.
4685              
4686             =item C<prune_unknown =E<gt> 1|0>
4687              
4688             Defaults to C<0>
4689              
4690             When set to a true value, unknown object properties in the instance are pruned (removed) prior to validation, based on the schema’s structural keywords.
4691              
4692             Pruning currently takes into account:
4693              
4694             =over 4
4695              
4696             =item * C<properties>
4697              
4698             =item * C<patternProperties>
4699              
4700             =item * C<additionalProperties>
4701              
4702             (item value or subschema, including within C<allOf>)
4703              
4704             =item * C<allOf> (for merging additional object or array constraints)
4705              
4706             =back
4707              
4708             For objects:
4709              
4710             =over 4
4711              
4712             =item *
4713              
4714             Any property explicitly declared under C<properties> is kept, and its value is recursively pruned according to its subschema (if it is itself an object or array).
4715              
4716             =item *
4717              
4718             Any property whose name matches one of the C<patternProperties> regular expressions is kept, and pruned recursively according to the associated subschema.
4719              
4720             =item *
4721              
4722             If C<additionalProperties> is C<false>, any object property not covered by C<properties> or C<patternProperties> is removed.
4723              
4724             =item *
4725              
4726             If C<additionalProperties> is a subschema, any such additional property is kept, and its value is pruned recursively following that subschema.
4727              
4728             =back
4729              
4730             For arrays:
4731              
4732             =over 4
4733              
4734             =item *
4735              
4736             Items covered by C<prefixItems> (by index) or C<items> (for remaining elements) are kept, and if they are objects or arrays, they are pruned recursively. Existing positions are never removed; pruning only affects the nested contents.
4737              
4738             =back
4739              
4740             The pruner intentionally does B<not> interpret C<anyOf>, C<oneOf> or C<not> when deciding which properties to keep or drop, because doing so would require running full validation logic and could remove legitimate data incorrectly. In those cases, pruning errs on the side of keeping more data rather than over-pruning.
4741              
4742             When C<prune_unknown> is disabled (the default), the instance is not modified for validation purposes, and no pruning is performed.
4743              
4744             =item C<trace>
4745              
4746             Defaults to C<0>
4747              
4748             Enable or disable tracing. When enabled, the validator records lightweight, bounded trace events according to L</trace_limit> and L</trace_sample>.
4749              
4750             =item C<trace_limit>
4751              
4752             Defaults to C<0>
4753              
4754             Set a hard cap on the number of trace entries recorded during a single C<validate> call (C<0> = unlimited).
4755              
4756             =item C<trace_sample =E<gt> $percent>
4757              
4758             Enable probabilistic sampling of trace events. C<$percent> is an integer percentage in C<[0,100]>. C<0> disables sampling. Sampling occurs per-event, and still respects L</trace_limit>.
4759              
4760             =item C<unique_keys =E<gt> 1|0>
4761              
4762             Defaults to C<0>
4763              
4764             Explicitly enable or disable the C<uniqueKeys> applicator.
4765              
4766             C<uniqueKeys> is a non-standard extension (proposed for future drafts) that enforces uniqueness of one or more properties across all objects in an array.
4767              
4768             "uniqueKeys": [ ["id"] ] # 'id' must be unique
4769             "uniqueKeys": [ ["id"], ["email"] ] # id AND email must each be unique
4770             "uniqueKeys": [ ["category", "code"] ] # the pair (category,code) must be unique
4771              
4772             The applicator supports both single-property constraints and true composite keys.
4773              
4774             This option is useful when you need stronger guarantees than C<uniqueItems> provides, without resorting to complex C<contains>/C<not> patterns.
4775              
4776             When C<extensions> is enabled, C<unique_keys> is automatically turned on; the specific method allows finer-grained control.
4777              
4778             This works in B<both interpreted and compiled modes> and is fully integrated into the annotation system (plays nicely with C<unevaluatedProperties>, etc.).
4779              
4780             Disabled by default for maximum spec purity.
4781              
4782             =item C<vocab_support =E<gt> {}>
4783              
4784             A hash reference of support vocabularies.
4785              
4786             =back
4787              
4788             =head1 METHODS
4789              
4790             =head2 compile
4791              
4792             $js->compile; # enable compilation
4793             $js->compile(1); # enable
4794             $js->compile(0); # disable
4795              
4796             Enable or disable the compiled-validator fast path.
4797              
4798             When enabled and the root hasn’t been compiled yet, this triggers an initial compilation.
4799              
4800             Returns the current object to enable chaining.
4801              
4802             =head2 compile_js
4803              
4804             my $js_source = $js->compile_js;
4805             my $js_source = $js->compile_js( ecma => 2018 );
4806              
4807             Generate a standalone JavaScript validator for the current schema and return it as a UTF-8 string.
4808              
4809             You are responsible for writing this string to a C<.js> file and serving it to the browser (or embedding it in a page).
4810              
4811             The generated code:
4812              
4813             =over 4
4814              
4815             =item *
4816              
4817             Wraps everything in a simple IIFE (Immediately Invoked Function Expression) C<(function(global){ ... })(this)>.
4818              
4819             =item *
4820              
4821             Defines a single public function:
4822              
4823             function validate(inst) { ... }
4824              
4825             exported on the global object (C<window.validate> in a browser).
4826              
4827             =item *
4828              
4829             Implements the same error reporting format as the Perl engine, but using plain JavaScript objects:
4830              
4831             {
4832             path: "#/path/in/instance",
4833             keyword: "minimum",
4834             message: "number is less than minimum 2",
4835             schema_pointer: "#/definitions/.../minimum"
4836             }
4837              
4838             =item *
4839              
4840             Returns an C<Array> of such error objects. If validation succeeds, the array is empty.
4841              
4842             =back
4843              
4844             Supported JavaScript options:
4845              
4846             =over 4
4847              
4848             =item * C<ecma =E<gt> "auto" | YEAR>
4849              
4850             Controls which JavaScript regexp features the generated code will try to use.
4851              
4852             ecma => "auto" # default
4853             ecma => 2018 # assume ES2018+ (Unicode property escapes, etc.)
4854              
4855             When C<ecma> is a number C<E<gt>= 2018>, patterns that use Unicode property escapes (e.g. C<\p{scx=Katakana}>) are compiled with the C</u> flag and will take advantage of Script / Script_Extensions support when the browser has it.
4856              
4857             In C<"auto"> mode the generator emits cautious compatibility shims: “advanced” patterns are wrapped in C<try/catch>; if the browser cannot compile them, those checks are silently skipped on the client (and are still enforced server-side by Perl).
4858              
4859             =item * C<max_errors =E<gt> 200>
4860              
4861             Defaults to 200.
4862              
4863             Set the maximum number of errors to be recorded.
4864              
4865             =item * C<name =E<gt> "myValidator">
4866              
4867             Defaults to C<validate>
4868              
4869             Sets a custom name for the JavaScript validation function.
4870              
4871             =back
4872              
4873             =head3 JavaScript keyword coverage
4874              
4875             The generated JS implements a pragmatic subset of the Perl engine:
4876              
4877             =over 4
4878              
4879             =item * Types
4880              
4881             C<type> (including unions).
4882              
4883             =item * Constants / enumerations
4884              
4885             C<const> (primitive values only) and C<enum>.
4886              
4887             Complex object/array C<const> values are currently ignored client-side and enforced server-side only.
4888              
4889             =item * Numbers
4890              
4891             C<minimum>, C<maximum>, C<exclusiveMinimum>, C<exclusiveMaximum>.
4892              
4893             For better UX, numeric-looking strings such as C<"10"> or C<"3.14"> are coerced to numbers before applying bounds. Non-numeric values:
4894              
4895             =over 4
4896              
4897             =item *
4898              
4899             trigger a C<type> error (C<"expected number but found string">) when numeric keywords are present and no explicit C<type> is declared;
4900              
4901             =item *
4902              
4903             or are handled by the normal C<type> keyword if you explicitly declared C<< type => 'number' >> in the schema.
4904              
4905             =back
4906              
4907             =item * Strings
4908              
4909             C<minLength>, C<maxLength>, C<pattern>.
4910              
4911             Patterns are converted from Perl syntax to JavaScript using a conservative converter (e.g. C<\x{FF70}> to C<\uFF70>, C<\p{Katakana}> to C<\p{scx=Katakana}>). When the browser does not support the necessary Unicode features, such patterns are skipped client-side.
4912              
4913             =item * Arrays
4914              
4915             C<items> (single-schema form), C<minItems>, C<maxItems>, C<contains>, C<minContains>, C<maxContains>.
4916              
4917             =item * Objects
4918              
4919             C<properties>, C<required>.
4920              
4921             =item * Combinators
4922              
4923             C<allOf>, C<anyOf>, C<oneOf>, C<not>.
4924              
4925             “Negative required” patterns of the form C<< { "not": { "required": [...] } } >> are intentionally skipped on the client and enforced server-side only.
4926              
4927             =item * Conditionals
4928              
4929             C<if>, C<then>, C<else>, with the same semantics as the Perl engine: C<if> is evaluated in a “shadow” context and never produces errors directly; only C<then>/C<else> do.
4930              
4931             =item * Non-core extension
4932              
4933             C<uniqueKeys> when you enabled it via C<< unique_keys => 1 >> or C<< ->unique_keys >>.
4934              
4935             =back
4936              
4937             The following are intentionally B<not> implemented in JavaScript (but are fully supported in Perl):
4938              
4939             =over 4
4940              
4941             =item *
4942              
4943             C<format> (client-side format checks are skipped).
4944              
4945             =item *
4946              
4947             C<prefixItems>, C<patternProperties>, C<unevaluatedItems>, C<unevaluatedProperties>, C<contentEncoding>, C<contentMediaType>, C<contentSchema>, external C<$ref> and C<$dynamicRef> targets.
4948              
4949             =back
4950              
4951             In other words: the JS validator is a fast, user-friendly I<pre-flight> check for web forms; the Perl validator remains the source of truth.
4952              
4953             =head3 Example: integrating the generated JS in a form
4954              
4955             Perl side:
4956              
4957             my $schema = ...; # your decoded schema
4958              
4959             my $validajstor = JSON::Schema::Validate->new( $schema )
4960             ->compile;
4961              
4962             my $js_source = $validator->compile_js( ecma => 2018 );
4963              
4964             open my $fh, '>:encoding(UTF-8)', 'htdocs/js/validator.js'
4965             or die( "Cannot write JS: $!" );
4966             print {$fh} $js_source;
4967             close $fh;
4968              
4969             HTML / JavaScript:
4970              
4971             <textarea id="company-data" rows="8" cols="80">
4972             { "name_ja": "株式会社テスト", "capital": 1 }
4973             </textarea>
4974              
4975             <button type="button" onclick="runValidation()">Validate</button>
4976              
4977             <pre id="validation-errors"></pre>
4978              
4979             <script src="/js/validator.js"></script>
4980             <script>
4981             function runValidation()
4982             {
4983             var src = document.getElementById('company-data').value;
4984             var out = document.getElementById('validation-errors');
4985             var inst;
4986              
4987             try
4988             {
4989             inst = JSON.parse( src );
4990             }
4991             catch( e )
4992             {
4993             out.textContent = "Invalid JSON: " + e;
4994             return;
4995             }
4996              
4997             var errors = validate( inst ); // defined by validator.js
4998              
4999             if( !errors || !errors.length )
5000             {
5001             out.textContent = "OK – no client-side schema errors.";
5002             return;
5003             }
5004              
5005             var lines = [];
5006             for( var i = 0; i < errors.length; i++ )
5007             {
5008             var e = errors[i];
5009             lines.push(
5010             "- " + e.path +
5011             " [" + e.keyword + "]: " +
5012             e.message
5013             );
5014             }
5015             out.textContent = lines.join("\n");
5016             }
5017             </script>
5018              
5019             You can then map each error back to specific fields, translate C<message> via your own localisation layer, or forward the C<errors> array to your logging pipeline.
5020              
5021             =head2 content_checks
5022              
5023             $js->content_checks; # enable
5024             $js->content_checks(1); # enable
5025             $js->content_checks(0); # disable
5026              
5027             Turn on/off content assertions for the C<contentEncoding>, C<contentMediaType> and C<contentSchema> trio.
5028              
5029             When enabling, built-in media validators are registered (e.g. C<application/json>).
5030              
5031             Returns the current object to enable chaining.
5032              
5033             =for Pod::Coverage enable_content_checks
5034              
5035             =head2 error
5036              
5037             my $msg = $js->error;
5038              
5039             Returns the first error L<JSON::Schema::Validate::Error> object out of all the possible errors found (see L</errors>), if any.
5040              
5041             When stringified, the object provides a short, human-oriented message for the first failure.
5042              
5043             =head2 errors
5044              
5045             my $array_ref = $js->errors;
5046              
5047             All collected L<error objects|JSON::Schema::Validate::Error> (up to the internal C<max_errors> cap).
5048              
5049             =head2 extensions
5050              
5051             $js->extensions; # enable all extensions
5052             $js->extensions(1); # enable
5053             $js->extensions(0); # disable
5054              
5055             Turn the extension framework on or off.
5056              
5057             Enabling extensions currently activates the C<uniqueKeys> applicator (and any future non-core features). Disabling it turns all extensions off, regardless of individual settings.
5058              
5059             Returns the object for method chaining.
5060              
5061             =head2 get_trace
5062              
5063             my $trace = $js->get_trace; # arrayref of trace entries (copy)
5064              
5065             Return a B<copy> of the last validation trace (array reference of hash references) so callers cannot mutate internal state. Each entry contains:
5066              
5067             {
5068             inst_path => '#/path/in/instance',
5069             keyword => 'node' | 'minimum' | ...,
5070             note => 'short string',
5071             outcome => 'pass' | 'fail' | 'visit' | 'start',
5072             schema_ptr => '#/path/in/schema',
5073             }
5074              
5075             =head2 get_trace_limit
5076              
5077             my $n = $js->get_trace_limit;
5078              
5079             Accessor that returns the numeric trace limit currently in effect. See L</trace_limit> to set it.
5080              
5081             =head2 ignore_unknown_required_vocab
5082              
5083             $js->ignore_unknown_required_vocab; # enable
5084             $js->ignore_unknown_required_vocab(1); # enable
5085             $js->ignore_unknown_required_vocab(0); # disable
5086              
5087             If enabled, required vocabularies declared in C<$vocabulary> that are not advertised as supported by the caller will be I<ignored> instead of causing the validator to C<die>.
5088              
5089             Returns the current object to enable chaining.
5090              
5091             =head2 is_compile_enabled
5092              
5093             my $bool = $js->is_compile_enabled;
5094              
5095             Read-only accessor.
5096              
5097             Returns true if compilation mode is enabled, false otherwise.
5098              
5099             =head2 is_content_checks_enabled
5100              
5101             my $bool = $js->is_content_checks_enabled;
5102              
5103             Read-only accessor.
5104              
5105             Returns true if content assertions are enabled, false otherwise.
5106              
5107             =head2 is_trace_on
5108              
5109             my $bool = $js->is_trace_on;
5110              
5111             Read-only accessor.
5112              
5113             Returns true if tracing is enabled, false otherwise.
5114              
5115             =head2 is_unique_keys_enabled
5116              
5117             my $bool = $js->is_unique_keys_enabled;
5118              
5119             Read-only accessor.
5120              
5121             Returns true if the C<uniqueKeys> applicator is currently active, false otherwise.
5122              
5123             =head2 is_unknown_required_vocab_ignored
5124              
5125             my $bool = $js->is_unknown_required_vocab_ignored;
5126              
5127             Read-only accessor.
5128              
5129             Returns true if unknown required vocabularies are being ignored, false otherwise.
5130              
5131             =head2 is_valid
5132              
5133             my $ok = $js->is_valid( $data );
5134              
5135             my $ok = $js->is_valid(
5136             $data,
5137             max_errors => 1, # default for is_valid
5138             trace_on => 0,
5139             trace_limit => 0,
5140             compile_on => 0,
5141             content_assert => 0,
5142             );
5143              
5144             Validate C<$data> against the compiled schema and return a boolean.
5145              
5146             This is a convenience method intended for “yes/no” checks. It behaves like L</validate> but defaults to C<< max_errors => 1 >> so that, on failure, only one error is recorded.
5147              
5148             On failure, the single recorded error can be retrieved with L</error>:
5149              
5150             $js->is_valid( $data )
5151             or die( $js->error );
5152              
5153             Per-call options are passed through to L</validate> and may override the instance configuration for this call only (e.g. C<max_errors>, C<trace_on>, C<trace_limit>, C<compile_on>, C<content_assert>).
5154              
5155             Returns 1 on success, 0 on failure.
5156              
5157             =head2 prune_instance
5158              
5159             my $pruned = $jsv->prune_instance( $instance );
5160              
5161             Returns a pruned copy of C<$instance> according to the schema that was passed to C<new>. The original data structure is B<not> modified.
5162              
5163             The pruning rules are the same as those used when the constructor option C<prune_unknown> is enabled (see L</prune_unknown>), namely:
5164              
5165             =over 4
5166              
5167             =item *
5168              
5169             For objects, only properties allowed by C<properties>, C<patternProperties> and C<additionalProperties> (including those brought in via C<allOf>) are kept. Their values are recursively pruned when they are objects or arrays.
5170              
5171             =item *
5172              
5173             If C<additionalProperties> is C<false>, properties not matched by C<properties> or C<patternProperties> are removed.
5174              
5175             =item *
5176              
5177             If C<additionalProperties> is a subschema, additional properties are kept and pruned recursively according to that subschema.
5178              
5179             =item *
5180              
5181             For arrays, items are never removed by index. However, for elements covered by C<prefixItems> or C<items>, their nested content is pruned recursively when it is an object or array.
5182              
5183             =item *
5184              
5185             C<anyOf>, C<oneOf> and C<not> are B<not> used to decide which properties to drop, to avoid over-pruning valid data without performing full validation.
5186              
5187             =back
5188              
5189             This method is useful when you want to clean incoming data structures before further processing, without necessarily performing a full schema validation at the same time.
5190              
5191             =head2 register_builtin_formats
5192              
5193             $js->register_builtin_formats;
5194              
5195             Registers the built-in validators listed in L</Formats>. Existing user-supplied format callbacks are preserved if they already exist under the same name.
5196              
5197             User-supplied callbacks passed via C<< format => { ... } >> are preserved and take precedence.
5198              
5199             =head2 register_content_decoder
5200              
5201             $js->register_content_decoder( $name => sub{ ... } );
5202              
5203             or
5204              
5205             $js->register_content_decoder(rot13 => sub
5206             {
5207             $bytes =~ tr/A-Za-z/N-ZA-Mn-za-m/;
5208             return( $bytes ); # now treated as (1, undef, $decoded)
5209             });
5210              
5211             Register a content B<decoder> for C<contentEncoding>. The callback receives a single argument: the raw data, and should return one of:
5212              
5213             =over 4
5214              
5215             =item * a decoded scalar (success);
5216              
5217             =item * C<undef> (failure);
5218              
5219             =item * or the triplet C<( $ok, $msg, $out )> where C<$ok> is truthy on success, C<$msg> is an optional error string, and C<$out> is the decoded value.
5220              
5221             =back
5222              
5223             The C<$name> is lower-cased internally. Returns the current object.
5224              
5225             Throws an exception if the second argument is not a code reference.
5226              
5227             =head2 register_format
5228              
5229             $js->register_format( $name, sub { ... } );
5230              
5231             Register or override a C<format> validator at runtime. The sub receives a single scalar (the candidate string) and must return true/false.
5232              
5233             =head2 register_media_validator
5234              
5235             $js->register_media_validator( 'application/json' => sub{ ... } );
5236              
5237             Register a media B<validator/decoder> for C<contentMediaType>. The callback receives 2 arguments:
5238              
5239             =over 4
5240              
5241             =item * C<$bytes>
5242              
5243             The data to validate
5244              
5245             =item * C<\%params>
5246              
5247             A hash reference of media-type parameters (e.g. C<charset>).
5248              
5249             =back
5250              
5251             It may return one of:
5252              
5253             =over 4
5254              
5255             =item * C<( $ok, $msg, $decoded )> — canonical form. On success C<$ok> is true, C<$msg> is optional, and C<$decoded> can be either a Perl structure or a new octet/string value.
5256              
5257             =item * a reference — treated as success with that reference as C<$decoded>.
5258              
5259             =item * a defined scalar — treated as success with that scalar as C<$decoded>.
5260              
5261             =item * C<undef> or empty list — treated as failure.
5262              
5263             =back
5264              
5265             The media type key is lower-cased internally.
5266              
5267             It returns the current object.
5268              
5269             It throws an exception if the second argument is not a code reference.
5270              
5271             =head2 set_comment_handler
5272              
5273             $js->set_comment_handler(sub
5274             {
5275             my( $schema_ptr, $text ) = @_;
5276             warn "Comment at $schema_ptr: $text\n";
5277             });
5278              
5279             Install an optional callback for the Draft 2020-12 C<$comment> keyword.
5280              
5281             C<$comment> is annotation-only (never affects validation). When provided, the callback is invoked once per encountered C<$comment> string with the schema pointer and the comment text. Callback errors are ignored.
5282              
5283             If a value is provided, and is not a code reference, a warning will be emitted.
5284              
5285             This returns the current object.
5286              
5287             =head2 set_resolver
5288              
5289             $js->set_resolver( sub{ my( $absolute_uri ) = @_; ...; return $schema_hashref } );
5290              
5291             Install a resolver for external documents. It is called with an absolute URI (formed from the current base C<$id> and the C<$ref>) and must return a Perl hash reference representation of a JSON Schema. If the returned hash contains C<'$id'>, it will become the new base for that document; otherwise, the absolute URI is used as its base.
5292              
5293             =head2 set_vocabulary_support
5294              
5295             $js->set_vocabulary_support( \%support );
5296              
5297             Declare which vocabularies the host supports, as a hash reference:
5298              
5299             {
5300             'https://example/vocab/core' => 1,
5301             ...
5302             }
5303              
5304             Resets internal vocabulary-checked state so the declaration is enforced on next C<validate>.
5305              
5306             By default, this module supports all vocabularies required by 2020-12.
5307              
5308             However, you can restrict support:
5309              
5310             $js->set_vocabulary_support({
5311             'https://json-schema.org/draft/2020-12/vocab/core' => 1,
5312             'https://json-schema.org/draft/2020-12/vocab/applicator' => 1,
5313             'https://json-schema.org/draft/2020-12/vocab/format' => 0,
5314             'https://mycorp/vocab/internal' => 1,
5315             });
5316              
5317             It returns the current object.
5318              
5319             =head2 trace
5320              
5321             $js->trace; # enable
5322             $js->trace(1); # enable
5323             $js->trace(0); # disable
5324              
5325             Enable or disable tracing. When enabled, the validator records lightweight, bounded trace events according to L</trace_limit> and L</trace_sample>.
5326              
5327             It returns the current object for chaining.
5328              
5329             =head2 trace_limit
5330              
5331             $js->trace_limit( $n );
5332              
5333             Set a hard cap on the number of trace entries recorded during a single C<validate> call (C<0> = unlimited).
5334              
5335             It returns the current object for chaining.
5336              
5337             =head2 trace_sample
5338              
5339             $js->trace_sample( $percent );
5340              
5341             Enable probabilistic sampling of trace events. C<$percent> is an integer percentage in C<[0,100]>. C<0> disables sampling. Sampling occurs per-event, and still respects L</trace_limit>.
5342              
5343             It returns the current object for chaining.
5344              
5345             =head2 unique_keys
5346              
5347             $js->unique_keys; # enable uniqueKeys
5348             $js->unique_keys(1); # enable
5349             $js->unique_keys(0); # disable
5350              
5351             Enable or disable the C<uniqueKeys> applicator independently of the C<extensions> option.
5352              
5353             When disabled (the default), schemas containing the C<uniqueKeys> keyword are ignored.
5354              
5355             Returns the object for method chaining.
5356              
5357             =head2 validate
5358              
5359             my $ok = $js->validate( $data );
5360              
5361             my $ok = $js->validate(
5362             $data,
5363             max_errors => 5,
5364             trace_on => 1,
5365             trace_limit => 200,
5366             compile_on => 0,
5367             content_assert => 1,
5368             );
5369              
5370             Validate a decoded JSON instance against the compiled schema and return a boolean.
5371              
5372             On failure, inspect C<< $js->error >> to retrieve the L<error object|JSON::Schema::Validate::Error> that stringifies for a concise message (first error), or C<< $js->errors >> for an array reference of L<error objects|JSON::Schema::Validate::Error>.
5373              
5374             Example:
5375              
5376             my $ok = $js->validate( $data ) or die( $js->error );
5377              
5378             Each error is a L<JSON::Schema::Validate::Error> object:
5379              
5380             my $err = $js->error;
5381             say $err->path; # #/properties~1name
5382             say $err->schema_pointer; # #/properties/name
5383             say $err->keyword; # minLength
5384             say $err->message; # string shorter than minLength 1
5385             say "$err"; # stringifies to a concise message
5386              
5387             =head3 Per-call option overrides
5388              
5389             C<validate> accepts optional named parameters (hash or hash reference) that override the validator’s instance configuration for this call only.
5390              
5391             Currently supported overrides:
5392              
5393             =over 4
5394              
5395             =item * C<max_errors>
5396              
5397             Maximum number of errors to collect before stopping validation.
5398              
5399             =item * C<trace_on>, C<trace_limit>
5400              
5401             Enable tracing and limit the number of trace entries.
5402              
5403             =item * C<compile_on>
5404              
5405             Enable on-the-fly compilation during validation.
5406              
5407             =item * C<content_assert>
5408              
5409             Enable media-type / content assertions.
5410              
5411             =back
5412              
5413             All options are optional and backward compatible. If omitted, the instance configuration is used.
5414              
5415             =head3 Relationship to C<is_valid>
5416              
5417             L</is_valid> is a convenience wrapper around C<validate> that defaults C<< max_errors => 1 >> and is intended for fast boolean checks:
5418              
5419             $js->is_valid( $data ) or die( $js->error );
5420              
5421             =head1 BEHAVIOUR NOTES
5422              
5423             =over 4
5424              
5425             =item * Recursion & Cycles
5426              
5427             The validator guards on the pair C<(schema_pointer, instance_address)>, so self-referential schemas and cyclic instance graphs will not infinite-loop.
5428              
5429             =item * Union Types with Inline Schemas
5430              
5431             C<type> may be an array mixing string type names and inline schemas. Any inline schema that validates the instance makes the C<type> check succeed.
5432              
5433             =item * Booleans
5434              
5435             For practicality in Perl, C<< type => 'boolean' >> accepts JSON-like booleans (e.g. true/false, 1/0 as strings) as well as Perl boolean objects (if you use a boolean class). If you need stricter behaviour, you can adapt C<_match_type> or introduce a constructor flag and branch there.
5436              
5437             =item * Combinators C<allOf>, C<anyOf>, C<oneOf>, C<not>
5438              
5439             C<allOf> validates all subschemas and merges their annotations (e.g. evaluated properties/items) into the parent schema’s annotation. If any subschema fails, C<allOf> fails.
5440              
5441             C<anyOf> and C<oneOf> always validate their branches in a “shadow” context.
5442             Errors produced by non-selected branches do not leak into the main context when the combinator as a whole succeeds. When C<anyOf> fails (no branch matched) or C<oneOf> fails (zero or more than one branch matched), the validator merges the collected branch errors into the main context to make debugging easier.
5443              
5444             C<not> is also evaluated in a shadow context. If the inner subschema validates, C<not> fails with a single “forbidden not-schema” error; otherwise C<not> succeeds and any inner errors are discarded.
5445              
5446             =item * Conditionals C<if> / C<then> / C<else>
5447              
5448             The subschema under C<if> is treated purely as a condition:
5449              
5450             =over 4
5451              
5452             =item *
5453              
5454             C<if> is always evaluated in an isolated “shadow” context. Any errors it produces (for example from C<required>) are never reported directly.
5455              
5456             =item *
5457              
5458             If C<if> succeeds and C<then> is present, C<then> is evaluated against the real context and may produce errors.
5459              
5460             =item *
5461              
5462             If C<if> fails and C<else> is present, C<else> is evaluated against the real context and may produce errors.
5463              
5464             =back
5465              
5466             This matches the JSON Schema 2020-12 intent: only C<then>/C<else> affect validity, C<if> itself never does.
5467              
5468             =item * C<unevaluatedItems> / C<unevaluatedProperties>
5469              
5470             Both C<unevaluatedItems> and C<unevaluatedProperties> are enforced using annotation produced by earlier keyword evaluations within the same schema object, matching draft 2020-12 semantics.
5471              
5472             =item * Error reporting and pointers
5473              
5474             Each error object contains both:
5475              
5476             =over 4
5477              
5478             =item *
5479              
5480             C<path> – a JSON Pointer-like path to the failing location in the instance (e.g. C<#/properties~1s/oneOf~11/properties~1classes/0>).
5481              
5482             =item *
5483              
5484             C<schema_pointer> – a JSON Pointer into the root schema that identifies the keyword which emitted the error (e.g.
5485             C<#/properties~1s/oneOf~11/properties~1classes/items/allOf~10/then/voting_right>).
5486              
5487             =back
5488              
5489             Messages for C<required> errors also list the full required set and the keys actually present at that location to help debug combinators such as C<anyOf>/C<oneOf>/C<if>/C<then>/C<else>.
5490              
5491             =item * RFC rigor and media types
5492              
5493             L<URI>/C<IRI> and media‐type parsing is intentionally pragmatic rather than fully RFC-complete. For example, C<uri>, C<iri>, and C<uri-reference> use strict but heuristic regexes; C<contentMediaType> validates UTF-8 for C<text/*; charset=utf-8> and supports pluggable validators/decoders, but is not a general MIME toolkit.
5494              
5495             =item * Compilation vs. Interpretation
5496              
5497             Both code paths are correct by design. The interpreter is simpler and great while developing a schema; toggle C<< ->compile >> when moving to production or after the schema stabilises. You may enable compilation lazily (call C<compile> any time) or eagerly via the constructor (C<< compile => 1 >>).
5498              
5499             =back
5500              
5501             =head1 WHY ENABLE C<COMPILE>?
5502              
5503             When C<compile> is ON, the validator precompiles a tiny Perl closure for each schema node. At runtime, those closures:
5504              
5505             =over 4
5506              
5507             =item * avoid repeated hash lookups for keyword presence/values;
5508              
5509             =item * skip dispatch on absent keywords (branchless fast paths);
5510              
5511             =item * reuse precompiled child validators (arrays/objects/combinators);
5512              
5513             =item * reduce allocator churn by returning small, fixed-shape result hashes.
5514              
5515             =back
5516              
5517             In practice this improves steady-state throughput (especially for large/branchy schemas, or hot validation loops) and lowers tail latency by minimising per-instance work. The trade-offs are:
5518              
5519             =over 4
5520              
5521             =item * a one-time compile cost per node (usually amortised quickly);
5522              
5523             =item * a small memory footprint for closures (one per visited node).
5524              
5525             =back
5526              
5527             If you only validate once or twice against a tiny schema, compilation will not matter; for services, batch jobs, or streaming pipelines it typically yields a noticeable speedup. Always benchmark with your own schema+data.
5528              
5529             =head1 AUTHOR
5530              
5531             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
5532              
5533             =head1 SEE ALSO
5534              
5535             L<perl>, L<Time::Piece>, L<DateTime>, L<DateTime::Format::ISO8601>, L<Regexp::Common>, L<Net::IDN::Encode>, L<JSON::PP>
5536              
5537             L<JSON::Schema>, L<JSON::Validator>
5538              
5539             L<python-jsonschema|https://github.com/python-jsonschema/jsonschema>,
5540             L<fastjsonschema|https://github.com/horejsek/python-fastjsonschema>,
5541             L<Pydantic|https://docs.pydantic.dev>,
5542             L<RapidJSON Schema|https://rapidjson.org/md_doc_schema.html>
5543              
5544             L<https://json-schema.org/specification>
5545              
5546             =head1 COPYRIGHT & LICENSE
5547              
5548             Copyright(c) 2025 DEGUEST Pte. Ltd.
5549              
5550             All rights reserved.
5551              
5552             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
5553              
5554             =cut