jtd_fuzz/
lib.rs

1//! Generate fuzzed data from a JSON Type Definition schema.
2//!
3//! # Quick start
4//!
5//! Here's how you can use [`fuzz`] to generate dummy data from a schema.
6//!
7//! ```
8//! use serde_json::json;
9//! use rand::SeedableRng;
10//!
11//! // An example schema we can test against.
12//! let schema = jtd::Schema::from_serde_schema(serde_json::from_value(json!({
13//!     "properties": {
14//!         "name": { "type": "string" },
15//!         "createdAt": { "type": "timestamp" },
16//!         "favoriteNumbers": {
17//!             "elements": { "type": "uint8" }
18//!         }
19//!     }
20//! })).unwrap()).unwrap();
21//!
22//! // A hard-coded RNG, so that the output is predictable.
23//! let mut rng = rand_pcg::Pcg32::seed_from_u64(8927);
24//!
25//! assert_eq!(jtd_fuzz::fuzz(&schema, &mut rng), json!({
26//!     "name": "f",
27//!     "createdAt": "1931-10-18T16:37:09-03:03",
28//!     "favoriteNumbers": [166, 142]
29//! }));
30//! ```
31
32use jtd::{Schema, Type};
33use rand::seq::IteratorRandom;
34use serde_json::Value;
35use std::collections::{BTreeMap, BTreeSet};
36
37// Max length when generating "sequences" of things, such as strings, arrays,
38// and objects.
39const MAX_SEQ_LENGTH: u8 = 8;
40
41// Key in metadata that, if present and one of the recognized values, will
42// result in a specific sort of data being produced instead of the generic
43// default.
44const METADATA_KEY_FUZZ_HINT: &'static str = "fuzzHint";
45
46/// Generates a single random JSON value satisfying a given schema.
47///
48/// The generated output is purely a function of the given schema and RNG. It is
49/// guaranteed that the returned data satisfies the given schema.
50///
51/// # Invariants for generated data
52///
53/// The output of this function is not guaranteed to remain the same between
54/// different versions of this crate; if you use a different version of this
55/// crate, you may get different output from this function.
56///
57/// Some properties of fuzz which are guaranteed for this version of the crate,
58/// but which may change within the same major version number of the crate:
59///
60/// * Generated strings (for `type: string` and object keys), arrays (for
61///   `elements`), and objects (for `values`) will have no more than seven
62///   characters, elements, and members, respectively.
63///
64/// * No more than seven "extra" properties will be added for schemas with
65///   `additionalProperties`.
66///
67/// * Generated strings will be entirely printable ASCII.
68///
69/// * Generated timestamps will have a random offset from UTC. These offsets
70///   will not necessarily be "historical"; some offsets may never have been
71///   used in the real world.
72///
73/// # Using `fuzzHint`
74///
75/// If you want to generate a specific sort of string from your schema, you can
76/// use the `fuzzHint` metadata property to customize output. For example, if
77/// you'd like to generate a fake email instead of a generic string, you can use
78/// a `fuzzHint` of `en_us/internet/email`:
79///
80/// ```
81/// use serde_json::json;
82/// use rand::SeedableRng;
83///
84/// let schema = jtd::Schema::from_serde_schema(serde_json::from_value(json!({
85///     "type": "string",
86///     "metadata": {
87///         "fuzzHint": "en_us/internet/email"
88///     }
89/// })).unwrap()).unwrap();
90///
91/// let mut rng = rand_pcg::Pcg32::seed_from_u64(8927);
92/// assert_eq!(jtd_fuzz::fuzz(&schema, &mut rng), json!("prenner3@fay.com"));
93/// ```
94///
95/// `fuzzHint` will only be honored for schemas with `type` of `string`. It will
96/// not be honored for empty schemas. If `fuzzHint` does not have one of the
97/// values listed below, then its value will be ignored.
98///
99/// The possible values for `fuzzHint` are:
100///
101/// * [`en_us/addresses/city_name`][`faker_rand::en_us::addresses::CityName`]
102/// * [`en_us/addresses/division_abbreviation`][`faker_rand::en_us::addresses::DivisionAbbreviation`]
103/// * [`en_us/addresses/division`][`faker_rand::en_us::addresses::Division`]
104/// * [`en_us/addresses/postal_code`][`faker_rand::en_us::addresses::PostalCode`]
105/// * [`en_us/addresses/secondary_address`][`faker_rand::en_us::addresses::SecondaryAddress`]
106/// * [`en_us/addresses/street_address`][`faker_rand::en_us::addresses::StreetAddress`]
107/// * [`en_us/addresses/street_name`][`faker_rand::en_us::addresses::StreetName`]
108/// * [`en_us/company/company_name`][`faker_rand::en_us::company::CompanyName`]
109/// * [`en_us/company/slogan`][`faker_rand::en_us::company::Slogan`]
110/// * [`en_us/internet/domain`][`faker_rand::en_us::internet::Domain`]
111/// * [`en_us/internet/email`][`faker_rand::en_us::internet::Email`]
112/// * [`en_us/internet/username`][`faker_rand::en_us::internet::Username`]
113/// * [`en_us/names/first_name`][`faker_rand::en_us::names::FirstName`]
114/// * [`en_us/names/full_name`][`faker_rand::en_us::names::FullName`]
115/// * [`en_us/names/last_name`][`faker_rand::en_us::names::LastName`]
116/// * [`en_us/names/name_prefix`][`faker_rand::en_us::names::NamePrefix`]
117/// * [`en_us/names/name_suffix`][`faker_rand::en_us::names::NameSuffix`]
118/// * [`en_us/phones/phone_number`][`faker_rand::en_us::phones::PhoneNumber`]
119/// * [`fr_fr/addresses/address`][`faker_rand::fr_fr::addresses::Address`]
120/// * [`fr_fr/addresses/city_name`][`faker_rand::fr_fr::addresses::CityName`]
121/// * [`fr_fr/addresses/division`][`faker_rand::fr_fr::addresses::Division`]
122/// * [`fr_fr/addresses/postal_code`][`faker_rand::fr_fr::addresses::PostalCode`]
123/// * [`fr_fr/addresses/secondary_address`][`faker_rand::fr_fr::addresses::SecondaryAddress`]
124/// * [`fr_fr/addresses/street_address`][`faker_rand::fr_fr::addresses::StreetAddress`]
125/// * [`fr_fr/addresses/street_name`][`faker_rand::fr_fr::addresses::StreetName`]
126/// * [`fr_fr/company/company_name`][`faker_rand::fr_fr::company::CompanyName`]
127/// * [`fr_fr/internet/domain`][`faker_rand::fr_fr::internet::Domain`]
128/// * [`fr_fr/internet/email`][`faker_rand::fr_fr::internet::Email`]
129/// * [`fr_fr/internet/username`][`faker_rand::fr_fr::internet::Username`]
130/// * [`fr_fr/names/first_name`][`faker_rand::fr_fr::names::FirstName`]
131/// * [`fr_fr/names/full_name`][`faker_rand::fr_fr::names::FullName`]
132/// * [`fr_fr/names/last_name`][`faker_rand::fr_fr::names::LastName`]
133/// * [`fr_fr/names/name_prefix`][`faker_rand::fr_fr::names::NamePrefix`]
134/// * [`fr_fr/phones/phone_number`][`faker_rand::fr_fr::phones::PhoneNumber`]
135/// * [`lorem/word`][`faker_rand::lorem::Word`]
136/// * [`lorem/sentence`][`faker_rand::lorem::Sentence`]
137/// * [`lorem/paragraph`][`faker_rand::lorem::Paragraph`]
138/// * [`lorem/paragraphs`][`faker_rand::lorem::Paragraphs`]
139///
140/// New acceptable values for `fuzzHint` may be added to this crate within the
141/// same major version.
142pub fn fuzz<R: rand::Rng>(schema: &Schema, rng: &mut R) -> Value {
143    fuzz_with_root(schema, rng, schema)
144}
145
146fn fuzz_with_root<R: rand::Rng>(root: &Schema, rng: &mut R, schema: &Schema) -> Value {
147    match schema {
148        Schema::Empty { .. } => {
149            // Generate one of null, boolean, uint8, float64, string, the
150            // elements form, or the values form. The reasoning is that it's
151            // reasonable behavior, and has a good chance of helping users catch
152            // bugs.
153            //
154            // As a bit of a hack, we here try to detect if we are the fuzzing
155            // root schema. If we are, we will allow ourselves to generate
156            // structures which themselves will recursively contain more empty
157            // schemas. But those empty schemas in turn will not contain further
158            // empty schemas.
159            //
160            // Doing so helps us avoid overflowing the stack.
161            let range_max_value = if root as *const _ == schema as *const _ {
162                7 // 0 through 6
163            } else {
164                5 // 0 through 4
165            };
166
167            let val = rng.gen_range(0..range_max_value);
168            match val {
169                // 0-4 are cases we will always potentially generate.
170                0 => Value::Null,
171                1 => rng.gen::<bool>().into(),
172                2 => rng.gen::<u8>().into(),
173                3 => rng.gen::<f64>().into(),
174                4 => fuzz_string(rng).into(),
175
176                // All the following cases are "recursive" cases. See above for
177                // why it's important these come after the "primitive" cases.
178                5 => {
179                    let schema = Schema::Elements {
180                        metadata: Default::default(),
181                        definitions: Default::default(),
182                        nullable: false,
183                        elements: Box::new(Schema::Empty {
184                            metadata: Default::default(),
185                            definitions: Default::default(),
186                        }),
187                    };
188
189                    fuzz(&schema, rng)
190                }
191
192                6 => {
193                    let schema = Schema::Values {
194                        metadata: Default::default(),
195                        definitions: Default::default(),
196                        nullable: false,
197                        values: Box::new(Schema::Empty {
198                            metadata: Default::default(),
199                            definitions: Default::default(),
200                        }),
201                    };
202
203                    fuzz(&schema, rng)
204                }
205
206                _ => unreachable!(),
207            }
208        }
209
210        Schema::Ref {
211            ref ref_, nullable, ..
212        } => {
213            if *nullable && rng.gen() {
214                return Value::Null;
215            }
216
217            fuzz_with_root(root, rng, &root.definitions()[ref_])
218        }
219
220        Schema::Type {
221            ref metadata,
222            ref type_,
223            nullable,
224            ..
225        } => {
226            if *nullable && rng.gen() {
227                return Value::Null;
228            }
229
230            match type_ {
231                Type::Boolean => rng.gen::<bool>().into(),
232                Type::Float32 => rng.gen::<f32>().into(),
233                Type::Float64 => rng.gen::<f64>().into(),
234                Type::Int8 => rng.gen::<i8>().into(),
235                Type::Uint8 => rng.gen::<u8>().into(),
236                Type::Int16 => rng.gen::<i16>().into(),
237                Type::Uint16 => rng.gen::<u16>().into(),
238                Type::Int32 => rng.gen::<i32>().into(),
239                Type::Uint32 => rng.gen::<u32>().into(),
240                Type::String => {
241                    match metadata.get(METADATA_KEY_FUZZ_HINT).and_then(Value::as_str) {
242                        Some("en_us/addresses/address") => rng
243                            .gen::<faker_rand::en_us::addresses::Address>()
244                            .to_string()
245                            .into(),
246                        Some("en_us/addresses/city_name") => rng
247                            .gen::<faker_rand::en_us::addresses::CityName>()
248                            .to_string()
249                            .into(),
250                        Some("en_us/addresses/division") => rng
251                            .gen::<faker_rand::en_us::addresses::Division>()
252                            .to_string()
253                            .into(),
254                        Some("en_us/addresses/division_abbreviation") => rng
255                            .gen::<faker_rand::en_us::addresses::DivisionAbbreviation>()
256                            .to_string()
257                            .into(),
258                        Some("en_us/addresses/postal_code") => rng
259                            .gen::<faker_rand::en_us::addresses::PostalCode>()
260                            .to_string()
261                            .into(),
262                        Some("en_us/addresses/secondary_address") => rng
263                            .gen::<faker_rand::en_us::addresses::SecondaryAddress>()
264                            .to_string()
265                            .into(),
266                        Some("en_us/addresses/street_address") => rng
267                            .gen::<faker_rand::en_us::addresses::StreetAddress>()
268                            .to_string()
269                            .into(),
270                        Some("en_us/addresses/street_name") => rng
271                            .gen::<faker_rand::en_us::addresses::StreetName>()
272                            .to_string()
273                            .into(),
274                        Some("en_us/company/company_name") => rng
275                            .gen::<faker_rand::en_us::company::CompanyName>()
276                            .to_string()
277                            .into(),
278                        Some("en_us/company/slogan") => rng
279                            .gen::<faker_rand::en_us::company::Slogan>()
280                            .to_string()
281                            .into(),
282                        Some("en_us/internet/domain") => rng
283                            .gen::<faker_rand::en_us::internet::Domain>()
284                            .to_string()
285                            .into(),
286                        Some("en_us/internet/email") => rng
287                            .gen::<faker_rand::en_us::internet::Email>()
288                            .to_string()
289                            .into(),
290                        Some("en_us/internet/username") => rng
291                            .gen::<faker_rand::en_us::internet::Username>()
292                            .to_string()
293                            .into(),
294                        Some("en_us/names/first_name") => rng
295                            .gen::<faker_rand::en_us::names::FirstName>()
296                            .to_string()
297                            .into(),
298                        Some("en_us/names/full_name") => rng
299                            .gen::<faker_rand::en_us::names::FullName>()
300                            .to_string()
301                            .into(),
302                        Some("en_us/names/last_name") => rng
303                            .gen::<faker_rand::en_us::names::LastName>()
304                            .to_string()
305                            .into(),
306                        Some("en_us/names/name_prefix") => rng
307                            .gen::<faker_rand::en_us::names::NamePrefix>()
308                            .to_string()
309                            .into(),
310                        Some("en_us/names/name_suffix") => rng
311                            .gen::<faker_rand::en_us::names::NameSuffix>()
312                            .to_string()
313                            .into(),
314                        Some("en_us/phones/phone_number") => rng
315                            .gen::<faker_rand::en_us::phones::PhoneNumber>()
316                            .to_string()
317                            .into(),
318                        Some("fr_fr/addresses/address") => rng
319                            .gen::<faker_rand::fr_fr::addresses::Address>()
320                            .to_string()
321                            .into(),
322                        Some("fr_fr/addresses/city_name") => rng
323                            .gen::<faker_rand::fr_fr::addresses::CityName>()
324                            .to_string()
325                            .into(),
326                        Some("fr_fr/addresses/division") => rng
327                            .gen::<faker_rand::fr_fr::addresses::Division>()
328                            .to_string()
329                            .into(),
330                        Some("fr_fr/addresses/postal_code") => rng
331                            .gen::<faker_rand::fr_fr::addresses::PostalCode>()
332                            .to_string()
333                            .into(),
334                        Some("fr_fr/addresses/secondary_address") => rng
335                            .gen::<faker_rand::fr_fr::addresses::SecondaryAddress>()
336                            .to_string()
337                            .into(),
338                        Some("fr_fr/addresses/street_address") => rng
339                            .gen::<faker_rand::fr_fr::addresses::StreetAddress>()
340                            .to_string()
341                            .into(),
342                        Some("fr_fr/addresses/street_name") => rng
343                            .gen::<faker_rand::fr_fr::addresses::StreetName>()
344                            .to_string()
345                            .into(),
346                        Some("fr_fr/company/company_name") => rng
347                            .gen::<faker_rand::fr_fr::company::CompanyName>()
348                            .to_string()
349                            .into(),
350                        Some("fr_fr/internet/domain") => rng
351                            .gen::<faker_rand::fr_fr::internet::Domain>()
352                            .to_string()
353                            .into(),
354                        Some("fr_fr/internet/email") => rng
355                            .gen::<faker_rand::fr_fr::internet::Email>()
356                            .to_string()
357                            .into(),
358                        Some("fr_fr/internet/username") => rng
359                            .gen::<faker_rand::fr_fr::internet::Username>()
360                            .to_string()
361                            .into(),
362                        Some("fr_fr/names/first_name") => rng
363                            .gen::<faker_rand::fr_fr::names::FirstName>()
364                            .to_string()
365                            .into(),
366                        Some("fr_fr/names/full_name") => rng
367                            .gen::<faker_rand::fr_fr::names::FullName>()
368                            .to_string()
369                            .into(),
370                        Some("fr_fr/names/last_name") => rng
371                            .gen::<faker_rand::fr_fr::names::LastName>()
372                            .to_string()
373                            .into(),
374                        Some("fr_fr/names/name_prefix") => rng
375                            .gen::<faker_rand::fr_fr::names::NamePrefix>()
376                            .to_string()
377                            .into(),
378                        Some("fr_fr/phones/phone_number") => rng
379                            .gen::<faker_rand::fr_fr::phones::PhoneNumber>()
380                            .to_string()
381                            .into(),
382                        Some("lorem/word") => {
383                            rng.gen::<faker_rand::lorem::Word>().to_string().into()
384                        }
385                        Some("lorem/sentence") => {
386                            rng.gen::<faker_rand::lorem::Sentence>().to_string().into()
387                        }
388                        Some("lorem/paragraph") => {
389                            rng.gen::<faker_rand::lorem::Paragraph>().to_string().into()
390                        }
391                        Some("lorem/paragraphs") => rng
392                            .gen::<faker_rand::lorem::Paragraphs>()
393                            .to_string()
394                            .into(),
395
396                        _ => fuzz_string(rng).into(),
397                    }
398                }
399                Type::Timestamp => {
400                    use chrono::TimeZone;
401
402                    // We'll generate timestamps with some random seconds offset
403                    // from UTC. Most of these random offsets will never have
404                    // been used historically, but they can nonetheless be used
405                    // in valid RFC3339 timestamps.
406                    //
407                    // Although timestamp_millis accepts an i64, not all values
408                    // in that range are permissible. The i32 range is entirely
409                    // safe.
410                    //
411                    // However, UTC offsets present a practical complication:
412                    //
413                    // Java's java.time.ZoneOffset restricts offsets to no more
414                    // than 18 hours from UTC:
415                    //
416                    // https://docs.oracle.com/javase/8/docs/api/java/time/ZoneOffset.html
417                    //
418                    // .NET's System.DateTimeOffset restricts offsets to no more
419                    // than 14 hours from UTC:
420                    //
421                    // https://docs.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tooffset?view=net-5.0
422                    //
423                    // To make jtd-fuzz work out of the box with these
424                    // ecosystems, we will limit ourselves to the most selective
425                    // of these time ranges.
426                    let max_offset = 14 * 60 * 60;
427                    chrono::FixedOffset::east(rng.gen_range(-max_offset..=max_offset))
428                        .timestamp(rng.gen::<i32>() as i64, 0)
429                        .to_rfc3339()
430                        .into()
431                }
432            }
433        }
434
435        Schema::Enum {
436            ref enum_,
437            nullable,
438            ..
439        } => {
440            if *nullable && rng.gen() {
441                return Value::Null;
442            }
443
444            enum_.iter().choose(rng).unwrap().clone().into()
445        }
446
447        Schema::Elements {
448            ref elements,
449            nullable,
450            ..
451        } => {
452            if *nullable && rng.gen() {
453                return Value::Null;
454            }
455
456            (0..rng.gen_range(0..MAX_SEQ_LENGTH))
457                .map(|_| fuzz_with_root(root, rng, elements))
458                .collect::<Vec<_>>()
459                .into()
460        }
461
462        Schema::Properties {
463            ref properties,
464            ref optional_properties,
465            additional_properties,
466            nullable,
467            ..
468        } => {
469            if *nullable && rng.gen() {
470                return Value::Null;
471            }
472
473            let mut members = BTreeMap::new();
474
475            let mut required_keys: Vec<_> = properties.keys().cloned().collect();
476            required_keys.sort();
477
478            for k in required_keys {
479                let v = fuzz_with_root(root, rng, &properties[&k]);
480                members.insert(k, v);
481            }
482
483            let mut optional_keys: Vec<_> = optional_properties.keys().cloned().collect();
484            optional_keys.sort();
485
486            for k in optional_keys {
487                if rng.gen() {
488                    continue;
489                }
490
491                let v = fuzz_with_root(root, rng, &optional_properties[&k]);
492                members.insert(k, v);
493            }
494
495            if *additional_properties {
496                // Go's encoding/json package, which implements JSON
497                // serialization/deserialization, is case-insensitive on inputs.
498                //
499                // In order to generate fuzzed data that's compatible with Go,
500                // we'll avoid generating "additional" properties that are
501                // case-insensitively equal to any required or optional property
502                // from the schema.
503                //
504                // Since we'll only generate ASCII properties here, we don't
505                // need to worry about implementing proper Unicode folding.
506                let defined_properties_lowercase: BTreeSet<_> = properties
507                    .keys()
508                    .chain(optional_properties.keys())
509                    .map(|s| s.to_lowercase())
510                    .collect();
511
512                for _ in 0..rng.gen_range(0..MAX_SEQ_LENGTH) {
513                    let key = fuzz_string(rng);
514
515                    if !defined_properties_lowercase.contains(&key.to_lowercase()) {
516                        members.insert(
517                            key,
518                            fuzz(
519                                &Schema::Empty {
520                                    metadata: Default::default(),
521                                    definitions: Default::default(),
522                                },
523                                rng,
524                            ),
525                        );
526                    }
527                }
528            }
529
530            members
531                .into_iter()
532                .collect::<serde_json::Map<String, Value>>()
533                .into()
534        }
535
536        Schema::Values {
537            ref values,
538            nullable,
539            ..
540        } => {
541            if *nullable && rng.gen() {
542                return Value::Null;
543            }
544
545            (0..rng.gen_range(0..MAX_SEQ_LENGTH))
546                .map(|_| (fuzz_string(rng), fuzz_with_root(root, rng, values)))
547                .collect::<serde_json::Map<String, Value>>()
548                .into()
549        }
550
551        Schema::Discriminator {
552            ref mapping,
553            ref discriminator,
554            nullable,
555            ..
556        } => {
557            if *nullable && rng.gen() {
558                return Value::Null;
559            }
560
561            let (discriminator_value, sub_schema) = mapping.iter().choose(rng).unwrap();
562
563            let mut obj = fuzz_with_root(root, rng, sub_schema);
564            obj.as_object_mut().unwrap().insert(
565                discriminator.to_owned(),
566                discriminator_value.to_owned().into(),
567            );
568            obj
569        }
570    }
571}
572
573fn fuzz_string<R: rand::Rng>(rng: &mut R) -> String {
574    (0..rng.gen_range(0..MAX_SEQ_LENGTH))
575        .map(|_| rng.gen_range(32u8..=127u8) as char)
576        .collect::<String>()
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use serde_json::json;
583
584    #[test]
585    fn test_fuzz_empty() {
586        assert_valid_fuzz(json!({}));
587    }
588
589    #[test]
590    fn test_fuzz_ref() {
591        assert_valid_fuzz(json!({
592            "definitions": {
593                "a": { "type": "timestamp" },
594                "b": { "type": "timestamp", "nullable": true },
595                "c": { "ref": "b" },
596            },
597            "properties": {
598                "a": { "ref": "a" },
599                "b": { "ref": "b" },
600                "c": { "ref": "c" },
601            }
602        }));
603    }
604
605    #[test]
606    fn test_fuzz_type() {
607        assert_valid_fuzz(json!({ "type": "boolean" }));
608        assert_valid_fuzz(json!({ "type": "boolean", "nullable": true }));
609        assert_valid_fuzz(json!({ "type": "float32" }));
610        assert_valid_fuzz(json!({ "type": "float32", "nullable": true }));
611        assert_valid_fuzz(json!({ "type": "float64" }));
612        assert_valid_fuzz(json!({ "type": "float64", "nullable": true }));
613        assert_valid_fuzz(json!({ "type": "int8" }));
614        assert_valid_fuzz(json!({ "type": "int8", "nullable": true }));
615        assert_valid_fuzz(json!({ "type": "uint8" }));
616        assert_valid_fuzz(json!({ "type": "uint8", "nullable": true }));
617        assert_valid_fuzz(json!({ "type": "uint16" }));
618        assert_valid_fuzz(json!({ "type": "uint16", "nullable": true }));
619        assert_valid_fuzz(json!({ "type": "uint32" }));
620        assert_valid_fuzz(json!({ "type": "uint32", "nullable": true }));
621        assert_valid_fuzz(json!({ "type": "string" }));
622        assert_valid_fuzz(json!({ "type": "string", "nullable": true }));
623        assert_valid_fuzz(json!({ "type": "timestamp" }));
624        assert_valid_fuzz(json!({ "type": "timestamp", "nullable": true }));
625    }
626
627    #[test]
628    fn test_fuzz_enum() {
629        assert_valid_fuzz(json!({ "enum": ["a", "b", "c" ]}));
630        assert_valid_fuzz(json!({ "enum": ["a", "b", "c" ], "nullable": true }));
631    }
632
633    #[test]
634    fn test_fuzz_elements() {
635        assert_valid_fuzz(json!({ "elements": { "type": "uint8" }}));
636        assert_valid_fuzz(json!({ "elements": { "type": "uint8" }, "nullable": true }));
637    }
638
639    #[test]
640    fn test_fuzz_properties() {
641        assert_valid_fuzz(json!({
642            "properties": {
643                "a": { "type": "uint8" },
644                "b": { "type": "string" },
645            },
646            "optionalProperties": {
647                "c": { "type": "uint32" },
648                "d": { "type": "timestamp" },
649            },
650            "additionalProperties": true,
651            "nullable": true,
652        }));
653    }
654
655    #[test]
656    fn test_fuzz_values() {
657        assert_valid_fuzz(json!({ "values": { "type": "uint8" }}));
658        assert_valid_fuzz(json!({ "values": { "type": "uint8" }, "nullable": true }));
659    }
660
661    #[test]
662    fn test_fuzz_discriminator() {
663        assert_valid_fuzz(json!({
664            "discriminator": "version",
665            "mapping": {
666                "v1": {
667                    "properties": {
668                        "foo": { "type": "string" },
669                        "bar": { "type": "timestamp" }
670                    }
671                },
672                "v2": {
673                    "properties": {
674                        "foo": { "type": "uint8" },
675                        "bar": { "type": "float32" }
676                    }
677                }
678            },
679            "nullable": true,
680        }));
681    }
682
683    fn assert_valid_fuzz(schema: Value) {
684        use rand::SeedableRng;
685
686        let mut rng = rand_pcg::Pcg32::seed_from_u64(8927);
687        let schema = Schema::from_serde_schema(serde_json::from_value(schema).unwrap()).unwrap();
688
689        // Poor man's fuzzing.
690        for _ in 0..1000 {
691            let instance = super::fuzz(&schema, &mut rng);
692            let errors = jtd::validate(&schema, &instance, Default::default()).unwrap();
693            assert!(errors.is_empty(), "{}", instance);
694        }
695    }
696}