@@ -22,7 +22,7 @@ use JSON::MaybeXS 1.004001 'is_bool';
22
22
use Feature::Compat::Try;
23
23
use JSON::PP ();
24
24
use List::Util 1.33 qw( any none) ;
25
- use Scalar::Util ' blessed' ;
25
+ use Scalar::Util qw( blessed looks_like_number ) ;
26
26
use if " $] " >= 5.022, POSIX => ' isinf' ;
27
27
use Math::BigFloat;
28
28
use namespace::clean;
@@ -35,6 +35,7 @@ our $SHORT_CIRCUIT = 0;
35
35
our $MAX_TRAVERSAL_DEPTH = 50;
36
36
our $MOJO_BOOLEANS ; # deprecated; renamed to $SCALARREF_BOOLEANS
37
37
our $SCALARREF_BOOLEANS ;
38
+ our $STRINGY_NUMBERS ;
38
39
our $SPECIFICATION_VERSION ;
39
40
40
41
my %version_uris = (
@@ -55,6 +56,7 @@ sub evaluate {
55
56
local $SHORT_CIRCUIT = $_ [0]-> {short_circuit } // $SHORT_CIRCUIT ,
56
57
local $MAX_TRAVERSAL_DEPTH = $_ [0]-> {max_traversal_depth } // $MAX_TRAVERSAL_DEPTH ,
57
58
local $SCALARREF_BOOLEANS = $_ [0]-> {scalarref_booleans } // $SCALARREF_BOOLEANS // $_ [0]-> {mojo_booleans },
59
+ local $STRINGY_NUMBERS = $_ [0]-> {stringy_numbers } // $STRINGY_NUMBERS ,
58
60
local $SPECIFICATION_VERSION = $_ [0]-> {specification_version } // $SPECIFICATION_VERSION ,
59
61
shift
60
62
if blessed($_ [0]) and blessed($_ [0])-> isa(__PACKAGE__ );
@@ -415,6 +417,8 @@ sub _eval_keyword_type ($data, $schema, $state) {
415
417
my $type = get_type($data );
416
418
return 1 if any {
417
419
$type eq $_ or ($_ eq ' number' and $type eq ' integer' )
420
+ or ($type eq ' string' and $STRINGY_NUMBERS and looks_like_number($data )
421
+ and ($_ eq ' number' or ($_ eq ' integer' and $data == int ($data ))))
418
422
or ($_ eq ' boolean' and $SCALARREF_BOOLEANS and $type eq ' reference to SCALAR' )
419
423
} $schema -> {type }-> @*;
420
424
return E($state , ' got %s, not one of %s' , $type , join (' , ' , $schema -> {type }-> @*));
@@ -426,6 +430,8 @@ sub _eval_keyword_type ($data, $schema, $state) {
426
430
427
431
my $type = get_type($data );
428
432
return 1 if $type eq $schema -> {type } or ($schema -> {type } eq ' number' and $type eq ' integer' )
433
+ or ($type eq ' string' and $STRINGY_NUMBERS and looks_like_number($data )
434
+ and ($schema -> {type } eq ' number' or ($schema -> {type } eq ' integer' and $data == int ($data ))))
429
435
or ($schema -> {type } eq ' boolean' and $SCALARREF_BOOLEANS and $type eq ' reference to SCALAR' );
430
436
return E($state , ' got %s, not %s' , $type , $schema -> {type });
431
437
}
@@ -452,7 +458,9 @@ sub _eval_keyword_multipleOf ($data, $schema, $state) {
452
458
assert_keyword_type($state , $schema , ' number' );
453
459
abort($state , ' multipleOf value is not a positive number' ) if $schema -> {multipleOf } <= 0;
454
460
455
- return 1 if not is_type(' number' , $data );
461
+ return 1 if not is_type(' number' , $data )
462
+ and not ($STRINGY_NUMBERS and is_type(' string' , $data ) and looks_like_number($data )
463
+ and do { $data = 0+$data ; 1 });
456
464
457
465
# if either value is a float, use the bignum library for the calculation
458
466
if (ref ($data ) =~ / ^Math::Big(?:Int|Float)$ /
@@ -476,29 +484,33 @@ sub _eval_keyword_multipleOf ($data, $schema, $state) {
476
484
477
485
sub _eval_keyword_maximum ($data , $schema , $state ) {
478
486
assert_keyword_type($state , $schema , ' number' );
479
- return 1 if not is_type(' number' , $data );
480
- return 1 if $data <= $schema -> {maximum };
487
+ return 1 if not is_type(' number' , $data )
488
+ and not ($STRINGY_NUMBERS and is_type(' string' , $data ) and looks_like_number($data ));
489
+ return 1 if 0+$data <= $schema -> {maximum };
481
490
return E($state , ' value is larger than %s' , sprintf_num($schema -> {maximum }));
482
491
}
483
492
484
493
sub _eval_keyword_exclusiveMaximum ($data , $schema , $state ) {
485
494
assert_keyword_type($state , $schema , ' number' );
486
- return 1 if not is_type(' number' , $data );
487
- return 1 if $data < $schema -> {exclusiveMaximum };
495
+ return 1 if not is_type(' number' , $data )
496
+ and not ($STRINGY_NUMBERS and is_type(' string' , $data ) and looks_like_number($data ));
497
+ return 1 if 0+$data < $schema -> {exclusiveMaximum };
488
498
return E($state , ' value is equal to or larger than %s' , sprintf_num($schema -> {exclusiveMaximum }));
489
499
}
490
500
491
501
sub _eval_keyword_minimum ($data , $schema , $state ) {
492
502
assert_keyword_type($state , $schema , ' number' );
493
- return 1 if not is_type(' number' , $data );
494
- return 1 if $data >= $schema -> {minimum };
503
+ return 1 if not is_type(' number' , $data )
504
+ and not ($STRINGY_NUMBERS and is_type(' string' , $data ) and looks_like_number($data ));
505
+ return 1 if 0+$data >= $schema -> {minimum };
495
506
return E($state , ' value is smaller than %s' , sprintf_num($schema -> {minimum }));
496
507
}
497
508
498
509
sub _eval_keyword_exclusiveMinimum ($data , $schema , $state ) {
499
510
assert_keyword_type($state , $schema , ' number' );
500
- return 1 if not is_type(' number' , $data );
501
- return 1 if $data > $schema -> {exclusiveMinimum };
511
+ return 1 if not is_type(' number' , $data )
512
+ and not ($STRINGY_NUMBERS and is_type(' string' , $data ) and looks_like_number($data ));
513
+ return 1 if 0+$data > $schema -> {exclusiveMinimum };
502
514
return E($state , ' value is equal to or smaller than %s' , sprintf_num($schema -> {exclusiveMinimum }));
503
515
}
504
516
@@ -1366,10 +1378,36 @@ other, or badly-written schemas that could be optimized. Defaults to 50.
1366
1378
1367
1379
=head2 C<$SCALARREF_BOOLEANS >
1368
1380
1369
- When true, any type that is expected to be a boolean B<in the instance data > may also be expressed as
1381
+ When true, any value that is expected to be a boolean B<in the instance data > may also be expressed as
1370
1382
the scalar references C<\0 > or C<\1 > (which are serialized as booleans by JSON backends).
1371
1383
Defaults to false.
1372
1384
1385
+ =head2 C<$STRINGY_NUMBERS >
1386
+
1387
+ When true, any value that is expected to be a number or integer B<in the instance data > may also be
1388
+ expressed as a string. This does B<not > apply to the C<const > or C<enum > keywords, but only
1389
+ the following keywords:
1390
+
1391
+ =for :list
1392
+ * C<type > (where both C<string > and C<number > (and possibly C<integer > ) are considered types
1393
+ * C<multipleOf >
1394
+ * C<maximum >
1395
+ * C<exclusiveMaximum >
1396
+ * C<minimum >
1397
+ * C<exclusiveMinimum >
1398
+
1399
+ This allows you to write a schema like this (which validates a string representing an integer):
1400
+
1401
+ type: string
1402
+ pattern: ^[0-9]$
1403
+ multipleOf: 4
1404
+ minimum: 16
1405
+ maximum: 256
1406
+
1407
+ Such keywords are only applied if the value looks like a number, and do not generate a failure
1408
+ otherwise. Values are determined to be numbers via L<perlapi/looks_like_number> .
1409
+ Defaults to false.
1410
+
1373
1411
=head2 C<$SPECIFICATION_VERSION >
1374
1412
1375
1413
When set, the version of the draft specification is locked to one particular value, and use of
0 commit comments