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 11     11   5918 use strict;
  11         116  
  11         314  
4 11     11   56 use warnings;
  11         20  
  11         364  
5              
6 11     11   69 use B;
  11         19  
  11         660  
7 11     11   5696 use Mojo::Base -base;
  11         2149687  
  11         79  
8 11     11   13062 use Cpanel::JSON::XS;
  11         36802  
  11         706  
9 11     11   5654 use String::Random;
  11         36786  
  11         787  
10 11     11   5840 use Hash::Merge qw/ merge /;
  11         115938  
  11         771  
11 11     11   5367 use Data::Fake qw/ Core Names Text Dates /;
  11         13519  
  11         81  
12 11     11   198691 use JSON::Validator::Util qw/ schema_type /;
  11         709368  
  11         36894  
13              
14             our $VERSION = '0.19';
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 27     27 1 52235 my ( $self,%args ) = @_;
30              
31 27         77 my $schema = $args{schema}; # an already parsed JSON schema
32              
33 27 100       101 if ( ! $schema ) {
34             $schema = $args{schema_str} # an unparsed JSON schema
35 9   50     53 || die "json_schema_to_json needs schema or schema_str arg";
36              
37 9         81 eval { $schema = decode_json( $schema ); }
38 9 50       16 or do { die "json_schema_to_json failed to parse schema: $@" };
  0         0  
39             }
40              
41 27 50       193 $self->example_key( $args{example_key} ) if $args{example_key};
42              
43 27         127 $schema = $self->_validator->bundle({
44             replace => 1,
45             schema => $schema,
46             });
47              
48 26         233420 $self->_depth( $self->_depth + 1 );
49 26         264 my ( $method,$sub_schema ) = $self->_guess_method( $schema );
50 26 50       80 $self->_depth( $self->_depth - 1 ) if $self->_depth;
51              
52 26         205 return $self->$method( $sub_schema );
53             }
54              
55             sub _example_from_spec {
56 343     343   566 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 343 100 66     714 if $self->example_key && $schema->{ $self->example_key };
62              
63 341         1740 return ();
64             }
65              
66             sub _random_boolean {
67 3     3   10 my ( $self,$schema ) = @_;
68              
69 3 50       10 return $self->_example_from_spec( $schema )
70             if scalar $self->_example_from_spec( $schema );
71              
72 3 100       25 return rand > 0.5
73             ? Cpanel::JSON::XS::true
74             : Cpanel::JSON::XS::false
75             }
76              
77             sub _random_integer {
78 174     174   573 my ( $self,$schema ) = @_;
79              
80 174 50       354 return $self->_example_from_spec( $schema )
81             if scalar $self->_example_from_spec( $schema );
82              
83 174   100     420 my $min = $schema->{minimum} || 1;
84 174   100     363 my $max = $schema->{maximum} || 1000;
85              
86             # by default the min/max values are exclusive
87 174 100 66     623 $min++ if defined $min && $schema->{exclusiveMinimum};
88 174 100 66     611 $max-- if defined $max && $schema->{exclusiveMaximum};
89              
90 174 100       375 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         47 foreach my $int ( $min .. $max ) {
97 22 100       102 return $int if $int % $mof == 0;
98             }
99              
100             } else {
101 169         450 return fake_int( $min,$max )->();
102             }
103              
104 1         5 return undef;
105             }
106              
107             sub _random_number {
108 17     17   39 my ( $self,$schema ) = @_;
109              
110 17 50       44 return $self->_example_from_spec( $schema )
111             if scalar $self->_example_from_spec( $schema );
112              
113             return $self->_random_integer( $schema )
114 17 100       53 if ( $schema->{multipleOf} );
115              
116 15         39 return $self->_random_integer( $schema ) + $self->_random_integer( $schema ) / 10;
117             }
118              
119             sub _random_string {
120 148     148   277 my ( $self,$schema ) = @_;
121              
122 148 100       340 return $self->_example_from_spec( $schema )
123             if scalar $self->_example_from_spec( $schema );
124              
125 147 50 50     226 if ( my @enum = @{ $schema->{enum} // [] } ) {
  147         598  
126 0         0 return $self->_random_element( [ @enum ] );
127             }
128              
129             return $self->_str_rand->randregex( $schema->{pattern} )
130 147 100       350 if $schema->{pattern};
131              
132 145 100       334 if ( my $format = $schema->{format} ) {
133              
134 25         86 my $fake_name = fake_name()->();
135 25         836 $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 100         5358 "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 25         89 }->{ $format };
149             }
150              
151             my $min = $schema->{minLength}
152 120   66     435 || ( $schema->{maxLength} ? $schema->{maxLength} - 1 : 10 );
153              
154             my $max = $schema->{maxLength}
155 120   66     387 || ( $schema->{minLength} ? $schema->{minLength} + 1 : 50 );
156              
157 120         310 my $words = substr( fake_words( $max )->(),0,$max );
158 120         68148 return $words;
159             }
160              
161             sub _random_array {
162 16     16   40 my ( $self,$schema ) = @_;
163              
164 16         40 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     168 || ( $schema->{minItems} ? $schema->{minItems} + 1 : 5 )
      66        
171             });
172              
173 16 50 33     340 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         253 $self->_depth( $self->_depth + 1 );
179             }
180              
181 16         122 my @return_items;
182              
183 16 100       48 if ( my $items = $schema->{items} ) {
184              
185 12         38 $self->_depth( $self->_depth + 1 );
186              
187 12 100       99 if ( ref( $items ) eq 'ARRAY' ) {
188              
189 4         7 ADD_ITEM: foreach my $item ( @{ $items } ) {
  4         11  
190 15 50 33     33 last ADD_ITEM if ( $self->max_depth && $self->_depth >= $self->max_depth );
191 15 100       150 $self->_add_next_array_item( \@return_items,$item,$unique )
192             || redo ADD_ITEM; # possible halting problem
193             }
194              
195             } else {
196              
197 8         22 ADD_ITEM: foreach my $i ( 1 .. $length ) {
198 26 50 33     57 last ADD_ITEM if ( $self->max_depth && $self->_depth >= $self->max_depth );
199 26 100       278 $self->_add_next_array_item( \@return_items,$items,$unique )
200             || redo ADD_ITEM; # possible halting problem
201             }
202              
203             }
204              
205             } else {
206 4         26 @return_items = 1 .. $length;
207             }
208              
209 16 100       40 $self->_depth( $self->_depth - 1 ) if $self->_depth;
210 16         204 return [ @return_items ];
211             }
212              
213             sub _add_next_array_item {
214 41     41   96 my ( $self,$array,$schema,$unique ) = @_;
215              
216 41 50 33     82 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 41         425 $self->_depth( $self->_depth + 1 );
224 41         284 my ( $method,$sub_schema ) = $self->_guess_method( $schema );
225 41         128 my $value = $self->$method( $sub_schema );
226 41 100       366 $self->_depth( $self->_depth - 1 ) if $self->_depth;
227              
228 41 100       347 if ( ! $unique ) {
229 29         42 push( @{ $array },$value );
  29         82  
230 29         104 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 12         17 my %existing = map { $_ => 1 } @{ $array };
  17         34  
  12         33  
236              
237 12 100       31 if ( ! $existing{$value} ) {
238 10         23 push( @{ $array },$value );
  10         20  
239 10         28 return 1;
240             }
241              
242 2         7 return 0;
243             }
244              
245             sub _random_object {
246 35     35   76 my ( $self,$schema ) = @_;
247              
248 35         75 my $object = {};
249 35         57 my $required;
250 35         60 my %properties = map { $_ => 1 } keys( %{ $schema->{properties} } );
  244         501  
  35         160  
251              
252 35 100       129 if ( $required = $schema->{required} ) {
253             # we have a list of required properties, just use those
254 4         13 %properties = map { $_ => 1 } @{ $required };
  10         29  
  4         10  
255             }
256              
257             # check max/min properties requirements
258             my $min = $schema->{minProperties}
259 35   66     209 || ( $schema->{maxProperties} ? $schema->{maxProperties} - 1 : undef );
260              
261             my $max = $schema->{maxProperties}
262 35   66     155 || ( $schema->{minProperties} ? $schema->{minProperties} + 1 : undef );
263              
264 35 100 100     158 if ( ! $min && ! $max ) {
265             # no min or max, just make use of all properties
266 33         58 %properties = map { $_ => 1 } keys( %{ $schema->{properties} } );
  238         427  
  33         110  
267             }
268              
269 35 100 66     181 if ( $min && scalar( keys( %properties ) ) < $min ) {
270             # we have too few properties
271 1 50       8 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       9 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 35 100 100     106 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         5  
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     19 || ! grep { $_ eq $property } @{ $required }
  0         0  
  0         0  
294             );
295              
296 2 100       13 last MIN_PROP if scalar( keys( %properties ) ) <= $max;
297             }
298             }
299              
300 35 50 33     109 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 35         427 $self->_depth( $self->_depth + 1 );
306             }
307              
308 35         317 PROPERTY: foreach my $property ( keys %properties ) {
309              
310 241         1804 $self->_depth( $self->_depth + 1 );
311              
312 241 50 33     1931 last PROPERTY if ( $self->max_depth && $self->_depth >= $self->max_depth );
313              
314             my ( $method,$sub_schema )
315 241         2613 = $self->_guess_method( $schema->{properties}{$property} );
316              
317 241 100       1124 $object->{$property} = $self->can( $method )
318             ? $self->$method( $sub_schema )
319             : undef;
320             }
321              
322 35 100       218 $self->_depth( $self->_depth - 1 ) if $self->_depth;
323              
324 35         1301 return $object;
325             }
326              
327 0     0   0 sub _random_null { undef }
328              
329             sub _random_enum {
330 15     15   35 my ( $self,$schema ) = @_;
331 15         44 return $self->_random_element( $schema->{'enum'} );
332             }
333              
334             sub _guess_method {
335 313     313   592 my ( $self,$schema ) = @_;
336              
337 313 100 100     1289 if (
338             $schema->{'type'}
339             && ref( $schema->{'type'} ) eq 'ARRAY'
340             ) {
341 40         87 $schema->{'type'} = $self->_random_element( $schema->{'type'} );
342             }
343              
344             # check for combining schemas
345 313 100       1260 if ( my $any_of = $schema->{'anyOf'} ) {
    100          
    100          
    100          
346              
347             # easy, pick a random sub schema
348 1         5 my $sub_schema = $self->_random_element( $any_of );
349 1         6 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         3 my $merged_schema = {};
358              
359 1         2 foreach my $sub_schema ( @{ $all_of } ) {
  1         3  
360 2         387 $merged_schema = merge( $merged_schema,$sub_schema );
361             }
362              
363 1         154 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         115 warn __PACKAGE__ . " encountered oneOf, see CAVEATS perldoc section";
371 2         17 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         11 }->{ $not_type };
387              
388 1         7 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 308 100       861 ? 'enum'
401             : schema_type( $schema );
402              
403 308   50     2933 $schema_type //= 'null';
404              
405 308 50       583 $self->_depth( $self->_depth - 1 ) if $self->_depth;
406 308         3587 return ( "_random_$schema_type",$schema );
407             }
408              
409             sub _random_element {
410 56     56   90 my ( $self,$list ) = @_;
411 56         83 return $list->[ int( rand( scalar( @{ $list } ) ) ) ];
  56         196  
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.19
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 * additionalItems
500              
501             This is ignored
502              
503             =item * additionalProperties and patternProperties
504              
505             These are also ignored
506              
507             =item * dependencies
508              
509             This is *also* ignored, possible result of invalid JSON if used
510              
511             =item * oneOf
512              
513             Only the *first* schema from the oneOf list will be used (which means
514             that the data returned may be invalid against others in the list)
515              
516             =item * not
517              
518             Currently any not restrictions are ignored as these can be very hand wavy
519             but we will try a "best guess" in the case of "not" : { "type" : ... }
520              
521             =back
522              
523             In the case of oneOf and not the module will raise a warning to let you know that
524             potentially invalid JSON has been generated. If you're using this module then you
525             probably want to avoid oneOf and not in your schemas.
526              
527             It is also entirely possible to pass a schema that could never be validated, but
528             will result in a generated structure anyway, example: an integer that has a "minimum"
529             value of 2, "maximum" value of 4, and must be a "multipleOf" 5 - a nonsensical
530             combination.
531              
532             Note that the data generated is completely random, don't expect it to be the same
533             across runs or calls. The data is also meaningless in terms of what it represents
534             such that an object property of "name" that is a string will be generated as, for
535             example, "est sed asperiores" - The JSON generated is so you have a representative
536             B, not representative B. Set example keys in your schema and then
537             set the C in the constructor if you want this to be repeatable and/or
538             more representative.
539              
540             L is used for some of the generated data, through use of fake_name,
541             fake_past_datetime, fake_int, and fake_words
542              
543             To generate subsections of data, or for those schema that are large only generating
544             small sections, you can combine with L like so:
545              
546             use JSON::Validator;
547             my $jv = JSON::Validator->new;
548             $jv->schema( 'petstore.json' );
549              
550             my $generator = JSON::Schema::ToJSON->new;
551              
552             my $response = $generator->json_schema_to_json(
553             schema => $jv->get( '/definitions/Pet' )
554             );
555              
556             =head1 LICENSE
557              
558             This library is free software; you can redistribute it and/or modify it under
559             the same terms as Perl itself. If you would like to contribute documentation,
560             features, bug fixes, or anything else then please raise an issue / pull request:
561              
562             https://github.com/Humanstate/json-schema-tojson
563              
564             =head1 AUTHOR
565              
566             Lee Johnson - C
567              
568             =cut
569              
570             1;
571              
572             # vim:noet:sw=4:ts=4