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}