File Coverage

blib/lib/JSON/Schema/ToJSON.pm
Criterion Covered Total %
statement 160 192 83.3
branch 74 100 74.0
condition 42 70 60.0
subroutine 21 22 95.4
pod 1 1 100.0
total 298 385 77.4


line stmt bran cond sub pod time code
1             package JSON::Schema::ToJSON;
2              
3 11     11   5012 use strict;
  11         79  
  11         266  
4 11     11   50 use warnings;
  11         17  
  11         310  
5              
6 11     11   58 use B;
  11         16  
  11         524  
7 11     11   4489 use Mojo::Base -base;
  11         1886604  
  11         72  
8 11     11   10519 use Cpanel::JSON::XS;
  11         31944  
  11         621  
9 11     11   4413 use String::Random;
  11         30787  
  11         648  
10 11     11   4868 use Hash::Merge qw/ merge /;
  11         97488  
  11         647  
11 11     11   4540 use Data::Fake qw/ Core Names Text Dates /;
  11         10763  
  11         62  
12 11     11   164495 use JSON::Validator::Util qw/ schema_type /;
  11         591505  
  11         30251  
13              
14             our $VERSION = '0.20';
15              
16             has _validator => sub {
17             $ENV{JSON_VALIDATOR_RECURSION_LIMIT} = shift->max_depth;
18             require JSON::Validator;
19             JSON::Validator->new
20             };
21              
22             has _str_rand => sub { String::Random->new };
23             has _depth => sub { 0 };
24              
25             has max_depth => sub { 10 };
26             has example_key => sub { 0 };
27              
28             sub json_schema_to_json {
29 18     18 1 20124 my ( $self,%args ) = @_;
30              
31 18         47 my $schema = $args{schema}; # an already parsed JSON schema
32              
33 18 100       59 if ( ! $schema ) {
34             $schema = $args{schema_str} # an unparsed JSON schema
35 9   50     37 || die "json_schema_to_json needs schema or schema_str arg";
36              
37 9         83 eval { $schema = decode_json( $schema ); }
38 9 50       12 or do { die "json_schema_to_json failed to parse schema: $@" };
  0         0  
39             }
40              
41 18 50       101 $self->example_key( $args{example_key} ) if $args{example_key};
42              
43 18         72 $schema = $self->_validator->bundle({
44             replace => 1,
45             schema => $schema,
46             });
47              
48 18         22591 $self->_depth( $self->_depth + 1 );
49 18         139 my ( $method,$sub_schema ) = $self->_guess_method( $schema );
50 18 50       44 $self->_depth( $self->_depth - 1 ) if $self->_depth;
51              
52 18         115 return $self->$method( $sub_schema );
53             }
54              
55             sub _example_from_spec {
56 109     109   167 my ( $self,$schema ) = @_;
57              
58             # spec/schema can contain examples that we could use as mock data
59              
60             return $schema->{ $self->example_key } # OpenAPI specific
61 109 100 66     228 if $self->example_key && $schema->{ $self->example_key };
62              
63 107         486 return ();
64             }
65              
66             sub _random_boolean {
67 1     1   3 my ( $self,$schema ) = @_;
68              
69 1 50       4 return $self->_example_from_spec( $schema )
70             if scalar $self->_example_from_spec( $schema );
71              
72 1 50       10 return rand > 0.5
73             ? Cpanel::JSON::XS::true
74             : Cpanel::JSON::XS::false
75             }
76              
77             sub _random_integer {
78 79     79   217 my ( $self,$schema ) = @_;
79              
80 79 50       155 return $self->_example_from_spec( $schema )
81             if scalar $self->_example_from_spec( $schema );
82              
83 79   100     174 my $min = $schema->{minimum} || 1;
84 79   100     186 my $max = $schema->{maximum} || 1000;
85              
86             # by default the min/max values are exclusive
87 79 100 66     4319 $min++ if defined $min && $schema->{exclusiveMinimum};
88 79 100 66     225 $max-- if defined $max && $schema->{exclusiveMaximum};
89              
90 79 100       157 if ( my $mof = $schema->{multipleOf} ) {
91              
92             # if we have multipleOf just return the first value that fits. note that
93             # there is a possible bug here and the JSON schema spec isn't clear about
94             # it - it's possible to have a multipleOf that would never be possible
95             # given certain minimum and maximum (e.g. 1 .. 3, multiple of 4)
96 3         9 foreach my $int ( $min .. $max ) {
97 12 100       28 return $int if $int % $mof == 0;
98             }
99              
100             } else {
101 76         176 return fake_int( $min,$max )->();
102             }
103              
104 1         4 return undef;
105             }
106              
107             sub _random_number {
108 7     7   12 my ( $self,$schema ) = @_;
109              
110 7 50       13 return $self->_example_from_spec( $schema )
111             if scalar $self->_example_from_spec( $schema );
112              
113             return $self->_random_integer( $schema )
114 7 50       23 if ( $schema->{multipleOf} );
115              
116 7         15 return $self->_random_integer( $schema ) + $self->_random_integer( $schema ) / 10;
117             }
118              
119             sub _random_string {
120 21     21   37 my ( $self,$schema ) = @_;
121              
122 21 100       49 return $self->_example_from_spec( $schema )
123             if scalar $self->_example_from_spec( $schema );
124              
125 20 50 50     28 if ( my @enum = @{ $schema->{enum} // [] } ) {
  20         101  
126 0         0 return $self->_random_element( [ @enum ] );
127             }
128              
129             return $self->_str_rand->randregex( $schema->{pattern} )
130 20 100       53 if $schema->{pattern};
131              
132 18 100       45 if ( my $format = $schema->{format} ) {
133              
134 7         18 my $fake_name = fake_name()->();
135 7         200 $fake_name =~ s/ //g;
136              
137             return {
138             "date-time" => fake_past_datetime( "%Y-%m-%dT%H:%M:%S.000Z" )->(),
139             "email" => "$fake_name\@gmail.com",
140             "hostname" => "www.$fake_name.com",
141 28         1236 "ipv4" => join( '.',map { $self->_random_integer({
142             minimum => 1,
143             maximum => 254,
144             }) } 1 .. 4 ),
145             "ipv6" => '2001:0db8:0000:0000:0000:0000:1428:57ab',
146             "uri" => "https://www.$fake_name.com",
147             "uriref" => "https://www.$fake_name.com",
148 7         19 }->{ $format };
149             }
150              
151             my $min = $schema->{minLength}
152 11   66     40 || ( $schema->{maxLength} ? $schema->{maxLength} - 1 : 10 );
153              
154             my $max = $schema->{maxLength}
155 11   66     43 || ( $schema->{minLength} ? $schema->{minLength} + 1 : 50 );
156              
157 11         31 my $words = substr( fake_words( $max )->(),0,$max );
158 11         10291 return $words;
159             }
160              
161             sub _random_array {
162 12     12   25 my ( $self,$schema ) = @_;
163              
164 12         23 my $unique = $schema->{uniqueItems};
165              
166             my $length = $self->_random_integer({
167             minimum => $schema->{minItems}
168             || ( $schema->{maxItems} ? $schema->{maxItems} - 1 : 1 ),
169             maximum => $schema->{maxItems}
170 12   66     136 || ( $schema->{minItems} ? $schema->{minItems} + 1 : 5 )
      66        
171             });
172              
173 12 50 33     252 if ( $self->max_depth && $self->_depth >= $self->max_depth ) {
174 0         0 warn __PACKAGE__
175 0         0 . " hit max depth (@{[ $self->max_depth ]}) in _random_array";
176 0         0 return [ 1 .. $length ];
177             } else {
178 12         164 $self->_depth( $self->_depth + 1 );
179             }
180              
181 12         76 my @return_items;
182              
183 12 100       31 if ( my $items = $schema->{items} ) {
184              
185 8         21 $self->_depth( $self->_depth + 1 );
186              
187 8 100       65 if ( ref( $items ) eq 'ARRAY' ) {
188              
189 4         8 ADD_ITEM: foreach my $item ( @{ $items } ) {
  4         10  
190 23 50 33     45 last ADD_ITEM if ( $self->max_depth && $self->_depth >= $self->max_depth );
191 23 100       219 $self->_add_next_array_item( \@return_items,$item,$unique )
192             || redo ADD_ITEM; # possible halting problem
193             }
194              
195             } else {
196              
197 4         13 ADD_ITEM: foreach my $i ( 1 .. $length ) {
198 22 50 33     43 last ADD_ITEM if ( $self->max_depth && $self->_depth >= $self->max_depth );
199 22 100       209 $self->_add_next_array_item( \@return_items,$items,$unique )
200             || redo ADD_ITEM; # possible halting problem
201             }
202              
203             }
204              
205             } else {
206 4         16 @return_items = 1 .. $length;
207             }
208              
209 12 100       30 $self->_depth( $self->_depth - 1 ) if $self->_depth;
210 12         113 return [ @return_items ];
211             }
212              
213             sub _add_next_array_item {
214 45     45   76 my ( $self,$array,$schema,$unique ) = @_;
215              
216 45 50 33     75 if ( $self->max_depth && $self->_depth >= $self->max_depth ) {
217 0         0 warn __PACKAGE__
218 0         0 . " hit max depth (@{[ $self->max_depth ]}) in _add_next_array_item";
219 0         0 push( @{ $array },undef );
  0         0  
220 0         0 return 1;
221             }
222              
223 45         376 $self->_depth( $self->_depth + 1 );
224 45         284 my ( $method,$sub_schema ) = $self->_guess_method( $schema );
225 45         109 my $value = $self->$method( $sub_schema );
226 45 100       354 $self->_depth( $self->_depth - 1 ) if $self->_depth;
227              
228 45 100       291 if ( ! $unique ) {
229 20         26 push( @{ $array },$value );
  20         62  
230 20         63 return 1;
231             }
232              
233             # unique requires us to check all existing elements of the array and only
234             # add the new value if it doesn't already exist
235 25         31 my %existing = map { $_ => 1 } @{ $array };
  52         97  
  25         43  
236              
237 25 100       53 if ( ! $existing{$value} ) {
238 11         13 push( @{ $array },$value );
  11         20  
239 11         31 return 1;
240             }
241              
242 14         38 return 0;
243             }
244              
245             sub _random_object {
246 7     7   27 my ( $self,$schema ) = @_;
247              
248 7         17 my $object = {};
249 7         13 my $required;
250 7         12 my %properties = map { $_ => 1 } keys( %{ $schema->{properties} } );
  37         64  
  7         25  
251              
252 7 100       51 if ( $required = $schema->{required} ) {
253             # we have a list of required properties, just use those
254 2         4 %properties = map { $_ => 1 } @{ $required };
  4         10  
  2         4  
255             }
256              
257             # check max/min properties requirements
258             my $min = $schema->{minProperties}
259 7   66     41 || ( $schema->{maxProperties} ? $schema->{maxProperties} - 1 : undef );
260              
261             my $max = $schema->{maxProperties}
262 7   66     33 || ( $schema->{minProperties} ? $schema->{minProperties} + 1 : undef );
263              
264 7 100 100     28 if ( ! $min && ! $max ) {
265             # no min or max, just make use of all properties
266 5         11 %properties = map { $_ => 1 } keys( %{ $schema->{properties} } );
  31         58  
  5         21  
267             }
268              
269 7 100 66     39 if ( $min && scalar( keys( %properties ) ) < $min ) {
270             # we have too few properties
271 1 50       26 if ( $max ) {
272             # add more properties until we have enough
273 1         2 MAX_PROP: foreach my $property ( keys( %{ $schema->{properties} } ) ) {
  1         4  
274 1         2 $properties{$property} = 1;
275 1 50       4 last MAX_PROP if scalar( keys( %properties ) ) == $min;
276             }
277             } else {
278             # no max, just make use of all properties
279 0         0 %properties = map { $_ => 1 } keys( %{ $schema->{properties} } );
  0         0  
  0         0  
280             }
281             }
282              
283 7 100 100     36 if ( $max && scalar( keys( %properties ) ) > $max ) {
284             # we have too many properties, delete some (except those required)
285             # until we are below the max permitted amount
286 1         2 MIN_PROP: foreach my $property ( keys( %{ $schema->{properties} } ) ) {
  1         3  
287              
288             delete( $properties{$property} ) if (
289             # we can delete, we don't have any required properties
290             ! $required
291              
292             # or this property is not amongst the list of required properties
293 2 50 33     6 || ! grep { $_ eq $property } @{ $required }
  0         0  
  0         0  
294             );
295              
296 2 100       9 last MIN_PROP if scalar( keys( %properties ) ) <= $max;
297             }
298             }
299              
300 7 50 33     25 if ( $self->max_depth && $self->_depth >= $self->max_depth ) {
301 0         0 warn __PACKAGE__
302 0         0 . " hit max depth (@{[ $self->max_depth ]}) in _random_object";
303 0         0 return {};
304             } else {
305 7         89 $self->_depth( $self->_depth + 1 );
306             }
307              
308 7         63 PROPERTY: foreach my $property ( keys %properties ) {
309              
310 34         509 $self->_depth( $self->_depth + 1 );
311              
312 34 50 33     231 last PROPERTY if ( $self->max_depth && $self->_depth >= $self->max_depth );
313              
314             my ( $method,$sub_schema )
315 34         341 = $self->_guess_method( $schema->{properties}{$property} );
316              
317 34 50       170 $object->{$property} = $self->can( $method )
318             ? $self->$method( $sub_schema )
319             : undef;
320             }
321              
322 7 100       52 $self->_depth( $self->_depth - 1 ) if $self->_depth;
323              
324 7         91 return $object;
325             }
326              
327 0     0   0 sub _random_null { undef }
328              
329             sub _random_enum {
330 24     24   42 my ( $self,$schema ) = @_;
331 24         47 return $self->_random_element( $schema->{'enum'} );
332             }
333              
334             sub _guess_method {
335 97     97   152 my ( $self,$schema ) = @_;
336              
337 97 100 66     363 if (
338             $schema->{'type'}
339             && ref( $schema->{'type'} ) eq 'ARRAY'
340             ) {
341 1         7 $schema->{'type'} = $self->_random_element( $schema->{'type'} );
342             }
343              
344             # check for combining schemas
345 97 50       345 if ( my $any_of = $schema->{'anyOf'} ) {
    50          
    50          
    50          
346              
347             # easy, pick a random sub schema
348 0         0 my $sub_schema = $self->_random_element( $any_of );
349 0         0 return $self->_guess_method( $sub_schema );
350              
351             } elsif ( my $all_of = $schema->{'allOf'} ) {
352              
353             # easy? mush these all together and assume the schema doesn't
354             # contain any contradictory information. note the mushing
355             # together needs to be a little bit smart to prevent stomping
356             # on any duplicate keys (hence Hash::Merge)
357 0         0 my $merged_schema = {};
358              
359 0         0 foreach my $sub_schema ( @{ $all_of } ) {
  0         0  
360 0         0 $merged_schema = merge( $merged_schema,$sub_schema );
361             }
362              
363 0         0 return $self->_guess_method( $merged_schema );
364              
365             } elsif ( my $one_of = $schema->{'oneOf'} ) {
366              
367             # difficult - we need to generate data that validates against
368             # one and *only* one of the rules, so here we make a poor
369             # attempt and just go by the first rule
370 0         0 warn __PACKAGE__ . " encountered oneOf, see CAVEATS perldoc section";
371 0         0 return $self->_guess_method( $one_of->[0] );
372              
373             } elsif ( my $not = $schema->{'not'} ) {
374              
375 0 0       0 if ( my $not_type = $not->{'type'} ) {
376              
377             my $type = {
378             "string" => "integer",
379             "integer" => "string",
380             "number" => "string",
381             "enum" => "string",
382             "boolean" => "string",
383             "null" => "integer",
384             "object" => "string",
385             "array" => "object",
386 0         0 }->{ $not_type };
387              
388 0         0 return $self->_guess_method( { type => $type } );
389             }
390              
391             # well i don't like this, because by implication it means
392             # the data can be anything but the listed one so it seems
393             # very handwavy in some cases.
394 0         0 warn __PACKAGE__ . " encountered not, see CAVEATS perldoc section";
395             }
396              
397             # note that enum was removed from _guess_data_type in JSON::Validator
398             # f290618f621a36db8f5010f8b99a42170dac820a so need to check for it here
399             my $schema_type = $schema->{enum}
400 97 100       265 ? 'enum'
401             : schema_type( $schema );
402              
403 97   50     437 $schema_type //= 'null';
404              
405 97 50       163 $self->_depth( $self->_depth - 1 ) if $self->_depth;
406 97         1279 return ( "_random_$schema_type",$schema );
407             }
408              
409             sub _random_element {
410 25     25   40 my ( $self,$list ) = @_;
411 25         29 return $list->[ int( rand( scalar( @{ $list } ) ) ) ];
  25         84  
412             }
413              
414             =encoding utf8
415              
416             =head1 NAME
417              
418             JSON::Schema::ToJSON - Generate example JSON structures from JSON Schema definitions
419              
420             =head1 VERSION
421              
422             0.20
423              
424             =head1 SYNOPSIS
425              
426             use JSON::Schema::ToJSON;
427              
428             my $to_json = JSON::Schema::ToJSON->new(
429             example_key => undef, # set to a key to take example from
430             max_depth => 10, # increase if you have very deep data structures
431             );
432              
433             my $perl_string_hash_or_arrayref = $to_json->json_schema_to_json(
434             schema => $already_parsed_json_schema, # either this
435             schema_str => '{ "type" : "boolean" }', # or this
436             );
437              
438             =head1 DESCRIPTION
439              
440             L is a class for generating "fake" or "example" JSON data
441             structures from JSON Schema structures.
442              
443             =head1 CONSTRUCTOR ARGUMENTS
444              
445             =head2 example_key
446              
447             The key that will be used to find example data for use in the returned structure. In
448             the case of the following schema:
449              
450             {
451             "type" : "object",
452             "properties" : {
453             "id" : {
454             "type" : "string",
455             "description" : "ID of the payment.",
456             "x-example" : "123ABC"
457             }
458             }
459             }
460              
461             Setting example_key to C will make the generator return the content of
462             the C<"x-example"> (123ABC) rather than a random string/int/etc. This is more so
463             for things like OpenAPI specifications.
464              
465             You can set this to any key you like, although be careful as you could end up with
466             invalid data being used (for example an integer field and then using the description
467             key as the content would not be sensible or valid).
468              
469             =head2 max_depth
470              
471             To prevent deep recursion due to circular references in JSON schemas the module has
472             a default max depth set to a very conservative level of 10. If you need to go deeper
473             than this then pass a larger value at object construction.
474              
475             Note that the underlying JSON schema parser, L now handles recursion
476             so you shouldn't have to worry too much about this - see its documentation for details.
477              
478             =head1 METHODS
479              
480             =head2 json_schema_to_json
481              
482             my $perl_string_hash_or_arrayref = $to_json->json_schema_to_json(
483             schema => $already_parsed_json_schema, # either this
484             schema_str => '{ "type" : "boolean" }', # or this
485             );
486              
487             Returns a randomly generated representative data structure that corresponds to the
488             passed JSON schema. Can take either an already parsed JSON Schema or the raw JSON
489             Schema string.
490              
491             =head1 CAVEATS
492              
493             Caveats? The implementation is incomplete as using some of the more edge case JSON
494             schema validation options may not generate representative JSON so they will not
495             validate against the schema on a round trip. These include:
496              
497             =over 4
498              
499             =item * $ref may not be resolved
500              
501             Broken as of JSON::Validator v5.0
502              
503             =item * additionalItems
504              
505             This is ignored
506              
507             =item * additionalProperties and patternProperties
508              
509             These are also ignored
510              
511             =item * dependencies
512              
513             This is *also* ignored, possible result of invalid JSON if used
514              
515             =item * oneOf
516              
517             Only the *first* schema from the oneOf list will be used (which means
518             that the data returned may be invalid against others in the list)
519              
520             =item * not
521              
522             Currently any not restrictions are ignored as these can be very hand wavy
523             but we will try a "best guess" in the case of "not" : { "type" : ... }
524              
525             =back
526              
527             In the case of oneOf and not the module will raise a warning to let you know that
528             potentially invalid JSON has been generated. If you're using this module then you
529             probably want to avoid oneOf and not in your schemas.
530              
531             It is also entirely possible to pass a schema that could never be validated, but
532             will result in a generated structure anyway, example: an integer that has a "minimum"
533             value of 2, "maximum" value of 4, and must be a "multipleOf" 5 - a nonsensical
534             combination.
535              
536             Note that the data generated is completely random, don't expect it to be the same
537             across runs or calls. The data is also meaningless in terms of what it represents
538             such that an object property of "name" that is a string will be generated as, for
539             example, "est sed asperiores" - The JSON generated is so you have a representative
540             B, not representative B. Set example keys in your schema and then
541             set the C in the constructor if you want this to be repeatable and/or
542             more representative.
543              
544             L is used for some of the generated data, through use of fake_name,
545             fake_past_datetime, fake_int, and fake_words
546              
547             To generate subsections of data, or for those schema that are large only generating
548             small sections, you can combine with L like so:
549              
550             use JSON::Validator;
551             my $jv = JSON::Validator->new;
552             $jv->schema( 'petstore.json' );
553              
554             my $generator = JSON::Schema::ToJSON->new;
555              
556             my $response = $generator->json_schema_to_json(
557             schema => $jv->get( '/definitions/Pet' )
558             );
559              
560             =head1 LICENSE
561              
562             This library is free software; you can redistribute it and/or modify it under
563             the same terms as Perl itself. If you would like to contribute documentation,
564             features, bug fixes, or anything else then please raise an issue / pull request:
565              
566             https://github.com/Humanstate/json-schema-tojson
567              
568             =head1 AUTHOR
569              
570             Lee Johnson - C
571              
572             =cut
573              
574             1;
575              
576             # vim:noet:sw=4:ts=4