File Coverage

blib/lib/JSON/Schema/ToJSON.pm
Criterion Covered Total %
statement 172 192 89.5
branch 83 100 83.0
condition 43 70 61.4
subroutine 21 22 95.4
pod 1 1 100.0
total 320 385 83.1


line stmt bran cond sub pod time code
1             package JSON::Schema::ToJSON;
2              
3 12     12   6546 use strict;
  12         94  
  12         361  
4 12     12   62 use warnings;
  12         19  
  12         412  
5              
6 12     12   75 use B;
  12         24  
  12         679  
7 12     12   5809 use Mojo::Base -base;
  12         2377460  
  12         86  
8 12     12   14715 use Cpanel::JSON::XS;
  12         40843  
  12         796  
9 12     12   6240 use String::Random;
  12         41161  
  12         846  
10 12     12   6409 use Hash::Merge qw/ merge /;
  12         131714  
  12         837  
11 12     12   6516 use Data::Fake qw/ Core Names Text Dates /;
  12         14674  
  12         90  
12 12     12   222775 use JSON::Validator::Util qw/ schema_type /;
  12         785568  
  12         42055  
13              
14             our $VERSION = '0.18';
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 29     29 1 61063 my ( $self,%args ) = @_;
30              
31 29         83 my $schema = $args{schema}; # an already parsed JSON schema
32              
33 29 100       110 if ( ! $schema ) {
34             $schema = $args{schema_str} # an unparsed JSON schema
35 9   50     46 || die "json_schema_to_json needs schema or schema_str arg";
36              
37 9         93 eval { $schema = decode_json( $schema ); }
38 9 50       15 or do { die "json_schema_to_json failed to parse schema: $@" };
  0         0  
39             }
40              
41 29 50       208 $self->example_key( $args{example_key} ) if $args{example_key};
42              
43 29         129 $schema = $self->_validator->bundle({
44             replace => 1,
45             schema => $schema,
46             });
47              
48 26         2372748 $self->_depth( $self->_depth + 1 );
49 26         275 my ( $method,$sub_schema ) = $self->_guess_method( $schema );
50 26 50       100 $self->_depth( $self->_depth - 1 ) if $self->_depth;
51              
52 26         249 return $self->$method( $sub_schema );
53             }
54              
55             sub _example_from_spec {
56 327     327   592 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 327 100 66     750 if $self->example_key && $schema->{ $self->example_key };
62              
63 325         2079 return ();
64             }
65              
66             sub _random_boolean {
67 3     3   13 my ( $self,$schema ) = @_;
68              
69 3 50       11 return $self->_example_from_spec( $schema )
70             if scalar $self->_example_from_spec( $schema );
71              
72 3 100       40 return rand > 0.5
73             ? Cpanel::JSON::XS::true
74             : Cpanel::JSON::XS::false
75             }
76              
77             sub _random_integer {
78 169     169   613 my ( $self,$schema ) = @_;
79              
80 169 50       355 return $self->_example_from_spec( $schema )
81             if scalar $self->_example_from_spec( $schema );
82              
83 169   100     446 my $min = $schema->{minimum} || 1;
84 169   100     380 my $max = $schema->{maximum} || 1000;
85              
86             # by default the min/max values are exclusive
87 169 100 66     690 $min++ if defined $min && $schema->{exclusiveMinimum};
88 169 100 66     646 $max-- if defined $max && $schema->{exclusiveMaximum};
89              
90 169 100       366 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 5         20 foreach my $int ( $min .. $max ) {
97 22 100       100 return $int if $int % $mof == 0;
98             }
99              
100             } else {
101 164         433 return fake_int( $min,$max )->();
102             }
103              
104 1         7 return undef;
105             }
106              
107             sub _random_number {
108 19     19   56 my ( $self,$schema ) = @_;
109              
110 19 50       55 return $self->_example_from_spec( $schema )
111             if scalar $self->_example_from_spec( $schema );
112              
113             return $self->_random_integer( $schema )
114 19 100       83 if ( $schema->{multipleOf} );
115              
116 17         48 return $self->_random_integer( $schema ) + $self->_random_integer( $schema ) / 10;
117             }
118              
119             sub _random_string {
120 135     135   265 my ( $self,$schema ) = @_;
121              
122 135 100       322 return $self->_example_from_spec( $schema )
123             if scalar $self->_example_from_spec( $schema );
124              
125 134 50 50     215 if ( my @enum = @{ $schema->{enum} // [] } ) {
  134         566  
126 0         0 return $self->_random_element( [ @enum ] );
127             }
128              
129             return $self->_str_rand->randregex( $schema->{pattern} )
130 134 100       313 if $schema->{pattern};
131              
132 132 100       310 if ( my $format = $schema->{format} ) {
133              
134 23         74 my $fake_name = fake_name()->();
135 23         760 $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 92         4825 "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 23         78 }->{ $format };
149             }
150              
151             my $min = $schema->{minLength}
152 109   66     359 || ( $schema->{maxLength} ? $schema->{maxLength} - 1 : 10 );
153              
154             my $max = $schema->{maxLength}
155 109   66     339 || ( $schema->{minLength} ? $schema->{minLength} + 1 : 50 );
156              
157 109         301 my $words = substr( fake_words( $max )->(),0,$max );
158 109         62676 return $words;
159             }
160              
161             sub _random_array {
162 16     16   43 my ( $self,$schema ) = @_;
163              
164 16         37 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 16   66     195 || ( $schema->{minItems} ? $schema->{minItems} + 1 : 5 )
      66        
171             });
172              
173 16 50 33     406 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 16         251 $self->_depth( $self->_depth + 1 );
179             }
180              
181 16         138 my @return_items;
182              
183 16 100       70 if ( my $items = $schema->{items} ) {
184              
185 12         50 $self->_depth( $self->_depth + 1 );
186              
187 12 100       132 if ( ref( $items ) eq 'ARRAY' ) {
188              
189 4         19 ADD_ITEM: foreach my $item ( @{ $items } ) {
  4         14  
190 18 50 33     46 last ADD_ITEM if ( $self->max_depth && $self->_depth >= $self->max_depth );
191 18 100       206 $self->_add_next_array_item( \@return_items,$item,$unique )
192             || redo ADD_ITEM; # possible halting problem
193             }
194              
195             } else {
196              
197 8         38 ADD_ITEM: foreach my $i ( 1 .. $length ) {
198 22 50 33     66 last ADD_ITEM if ( $self->max_depth && $self->_depth >= $self->max_depth );
199 22 100       277 $self->_add_next_array_item( \@return_items,$items,$unique )
200             || redo ADD_ITEM; # possible halting problem
201             }
202              
203             }
204              
205             } else {
206 4         11 @return_items = 1 .. $length;
207             }
208              
209 16 100       72 $self->_depth( $self->_depth - 1 ) if $self->_depth;
210 16         206 return [ @return_items ];
211             }
212              
213             sub _add_next_array_item {
214 40     40   90 my ( $self,$array,$schema,$unique ) = @_;
215              
216 40 50 33     100 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 40         539 $self->_depth( $self->_depth + 1 );
224 40         341 my ( $method,$sub_schema ) = $self->_guess_method( $schema );
225 40         174 my $value = $self->$method( $sub_schema );
226 40 100       331 $self->_depth( $self->_depth - 1 ) if $self->_depth;
227              
228 40 100       434 if ( ! $unique ) {
229 24         38 push( @{ $array },$value );
  24         88  
230 24         108 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 16         33 my %existing = map { $_ => 1 } @{ $array };
  31         77  
  16         35  
236              
237 16 100       43 if ( ! $existing{$value} ) {
238 10         18 push( @{ $array },$value );
  10         21  
239 10         37 return 1;
240             }
241              
242 6         20 return 0;
243             }
244              
245             sub _random_object {
246 34     34   84 my ( $self,$schema ) = @_;
247              
248 34         86 my $object = {};
249 34         50 my $required;
250 34         60 my %properties = map { $_ => 1 } keys( %{ $schema->{properties} } );
  239         461  
  34         181  
251              
252 34 100       135 if ( $required = $schema->{required} ) {
253             # we have a list of required properties, just use those
254 4         14 %properties = map { $_ => 1 } @{ $required };
  10         31  
  4         16  
255             }
256              
257             # check max/min properties requirements
258             my $min = $schema->{minProperties}
259 34   66     197 || ( $schema->{maxProperties} ? $schema->{maxProperties} - 1 : undef );
260              
261             my $max = $schema->{maxProperties}
262 34   66     207 || ( $schema->{minProperties} ? $schema->{minProperties} + 1 : undef );
263              
264 34 100 100     145 if ( ! $min && ! $max ) {
265             # no min or max, just make use of all properties
266 32         64 %properties = map { $_ => 1 } keys( %{ $schema->{properties} } );
  233         401  
  32         100  
267             }
268              
269 34 100 66     141 if ( $min && scalar( keys( %properties ) ) < $min ) {
270             # we have too few properties
271 1 50       5 if ( $max ) {
272             # add more properties until we have enough
273 1         2 MAX_PROP: foreach my $property ( keys( %{ $schema->{properties} } ) ) {
  1         6  
274 2         3 $properties{$property} = 1;
275 2 100       10 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 34 100 100     140 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         4  
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     9 || ! grep { $_ eq $property } @{ $required }
  0         0  
  0         0  
294             );
295              
296 2 100       7 last MIN_PROP if scalar( keys( %properties ) ) <= $max;
297             }
298             }
299              
300 34 50 33     123 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 34         441 $self->_depth( $self->_depth + 1 );
306             }
307              
308 34         336 PROPERTY: foreach my $property ( keys %properties ) {
309              
310 236         1685 $self->_depth( $self->_depth + 1 );
311              
312 236 50 33     1928 last PROPERTY if ( $self->max_depth && $self->_depth >= $self->max_depth );
313              
314             my ( $method,$sub_schema )
315 236         2545 = $self->_guess_method( $schema->{properties}{$property} );
316              
317 236 100       1118 $object->{$property} = $self->can( $method )
318             ? $self->$method( $sub_schema )
319             : undef;
320             }
321              
322 34 100       215 $self->_depth( $self->_depth - 1 ) if $self->_depth;
323              
324 34         1160 return $object;
325             }
326              
327 0     0   0 sub _random_null { undef }
328              
329             sub _random_enum {
330 18     18   52 my ( $self,$schema ) = @_;
331 18         61 return $self->_random_element( $schema->{'enum'} );
332             }
333              
334             sub _guess_method {
335 307     307   638 my ( $self,$schema ) = @_;
336              
337 307 100 100     1346 if (
338             $schema->{'type'}
339             && ref( $schema->{'type'} ) eq 'ARRAY'
340             ) {
341 40         96 $schema->{'type'} = $self->_random_element( $schema->{'type'} );
342             }
343              
344             # check for combining schemas
345 307 100       1114 if ( my $any_of = $schema->{'anyOf'} ) {
    100          
    100          
    100          
346              
347             # easy, pick a random sub schema
348 1         4 my $sub_schema = $self->_random_element( $any_of );
349 1         17 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 1         2 my $merged_schema = {};
358              
359 1         4 foreach my $sub_schema ( @{ $all_of } ) {
  1         4  
360 2         407 $merged_schema = merge( $merged_schema,$sub_schema );
361             }
362              
363 1         181 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 2         100 warn __PACKAGE__ . " encountered oneOf, see CAVEATS perldoc section";
371 2         18 return $self->_guess_method( $one_of->[0] );
372              
373             } elsif ( my $not = $schema->{'not'} ) {
374              
375 1 50       5 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 1         14 }->{ $not_type };
387              
388 1         6 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 302 100       944 ? 'enum'
401             : schema_type( $schema );
402              
403 302   50     3046 $schema_type //= 'null';
404              
405 302 50       615 $self->_depth( $self->_depth - 1 ) if $self->_depth;
406 302         3596 return ( "_random_$schema_type",$schema );
407             }
408              
409             sub _random_element {
410 59     59   115 my ( $self,$list ) = @_;
411 59         126 return $list->[ int( rand( scalar( @{ $list } ) ) ) ];
  59         204  
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.18
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             =head1 METHODS
476              
477             =head2 json_schema_to_json
478              
479             my $perl_string_hash_or_arrayref = $to_json->json_schema_to_json(
480             schema => $already_parsed_json_schema, # either this
481             schema_str => '{ "type" : "boolean" }', # or this
482             );
483              
484             Returns a randomly generated representative data structure that corresponds to the
485             passed JSON schema. Can take either an already parsed JSON Schema or the raw JSON
486             Schema string.
487              
488             =head1 CAVEATS
489              
490             Caveats? The implementation is incomplete as using some of the more edge case JSON
491             schema validation options may not generate representative JSON so they will not
492             validate against the schema on a round trip. These include:
493              
494             =over 4
495              
496             =item * additionalItems
497              
498             This is ignored
499              
500             =item * additionalProperties and patternProperties
501              
502             These are also ignored
503              
504             =item * dependencies
505              
506             This is *also* ignored, possible result of invalid JSON if used
507              
508             =item * oneOf
509              
510             Only the *first* schema from the oneOf list will be used (which means
511             that the data returned may be invalid against others in the list)
512              
513             =item * not
514              
515             Currently any not restrictions are ignored as these can be very hand wavy
516             but we will try a "best guess" in the case of "not" : { "type" : ... }
517              
518             =back
519              
520             In the case of oneOf and not the module will raise a warning to let you know that
521             potentially invalid JSON has been generated. If you're using this module then you
522             probably want to avoid oneOf and not in your schemas.
523              
524             It is also entirely possible to pass a schema that could never be validated, but
525             will result in a generated structure anyway, example: an integer that has a "minimum"
526             value of 2, "maximum" value of 4, and must be a "multipleOf" 5 - a nonsensical
527             combination.
528              
529             Note that the data generated is completely random, don't expect it to be the same
530             across runs or calls. The data is also meaningless in terms of what it represents
531             such that an object property of "name" that is a string will be generated as, for
532             example, "est sed asperiores" - The JSON generated is so you have a representative
533             B, not representative B. Set example keys in your schema and then
534             set the C in the constructor if you want this to be repeatable and/or
535             more representative.
536              
537             L is used for some of the generated data, through use of fake_name,
538             fake_past_datetime, fake_int, and fake_words
539              
540             To generate subsections of data, or for those schema that are large only generating
541             small sections, you can combine with L like so:
542              
543             use JSON::Validator;
544             my $jv = JSON::Validator->new;
545             $jv->schema( 'petstore.json' );
546              
547             my $generator = JSON::Schema::ToJSON->new;
548              
549             my $response = $generator->json_schema_to_json(
550             schema => $jv->get( '/definitions/Pet' )
551             );
552              
553             =head1 LICENSE
554              
555             This library is free software; you can redistribute it and/or modify it under
556             the same terms as Perl itself. If you would like to contribute documentation,
557             features, bug fixes, or anything else then please raise an issue / pull request:
558              
559             https://github.com/Humanstate/json-schema-tojson
560              
561             =head1 AUTHOR
562              
563             Lee Johnson - C
564              
565             =cut
566              
567             1;
568              
569             # vim:noet:sw=4:ts=4