apistos_schemars/
gen.rs

1/*!
2JSON Schema generator and settings.
3
4This module is useful if you want more control over how the schema generated than the [`schema_for!`] macro gives you.
5There are two main types in this module:
6* [`SchemaSettings`], which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented).
7* [`SchemaGenerator`], which manages the generation of a schema document.
8*/
9
10use crate::schema::*;
11use crate::{visit::*, JsonSchema, Map};
12use dyn_clone::DynClone;
13use serde::Serialize;
14use std::borrow::Cow;
15use std::collections::HashMap;
16use std::{any::Any, collections::HashSet, fmt::Debug};
17
18/// Settings to customize how Schemas are generated.
19///
20/// The default settings currently conform to [JSON Schema Draft 7](https://json-schema.org/specification-links.html#draft-7), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added.
21/// If you require your generated schemas to conform to draft 7, consider using the [`draft07`](#method.draft07) method.
22#[derive(Debug, Clone)]
23#[non_exhaustive]
24pub struct SchemaSettings {
25    /// If `true`, schemas for [`Option<T>`](Option) will include a `nullable` property.
26    ///
27    /// This is not part of the JSON Schema spec, but is used in Swagger/OpenAPI schemas.
28    ///
29    /// Defaults to `false`.
30    pub option_nullable: bool,
31    /// If `true`, schemas for [`Option<T>`](Option) will have `null` added to their [`type`](../schema/struct.SchemaObject.html#structfield.instance_type).
32    ///
33    /// Defaults to `true`.
34    pub option_add_null_type: bool,
35    /// A JSON pointer to the expected location of referenceable subschemas within the resulting root schema.
36    ///
37    /// Defaults to `"#/definitions/"`.
38    pub definitions_path: String,
39    /// The URI of the meta-schema describing the structure of the generated schemas.
40    ///
41    /// Defaults to `"http://json-schema.org/draft-07/schema#"`.
42    pub meta_schema: Option<String>,
43    /// A list of visitors that get applied to all generated root schemas.
44    pub visitors: Vec<Box<dyn GenVisitor>>,
45    /// Inline all subschemas instead of using references.
46    ///
47    /// Some references may still be generated in schemas for recursive types.
48    ///
49    /// Defaults to `false`.
50    pub inline_subschemas: bool,
51}
52
53impl Default for SchemaSettings {
54    fn default() -> SchemaSettings {
55        SchemaSettings::draft07()
56    }
57}
58
59impl SchemaSettings {
60    /// Creates `SchemaSettings` that conform to [JSON Schema Draft 7](https://json-schema.org/specification-links.html#draft-7).
61    pub fn draft07() -> SchemaSettings {
62        SchemaSettings {
63            option_nullable: false,
64            option_add_null_type: true,
65            definitions_path: "#/definitions/".to_owned(),
66            meta_schema: Some("http://json-schema.org/draft-07/schema#".to_owned()),
67            visitors: vec![Box::new(RemoveRefSiblings)],
68            inline_subschemas: false,
69        }
70    }
71
72    /// Creates `SchemaSettings` that conform to [JSON Schema 2019-09](https://json-schema.org/specification-links.html#2019-09-formerly-known-as-draft-8).
73    pub fn draft2019_09() -> SchemaSettings {
74        SchemaSettings {
75            option_nullable: false,
76            option_add_null_type: true,
77            definitions_path: "#/definitions/".to_owned(),
78            meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()),
79            visitors: Vec::default(),
80            inline_subschemas: false,
81        }
82    }
83
84    /// Creates `SchemaSettings` that conform to [OpenAPI 3.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject).
85    pub fn openapi3() -> SchemaSettings {
86        SchemaSettings {
87            option_nullable: true,
88            option_add_null_type: false,
89            definitions_path: "#/components/schemas/".to_owned(),
90            meta_schema: Some(
91                "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema"
92                    .to_owned(),
93            ),
94            visitors: vec![
95                Box::new(RemoveRefSiblings),
96                Box::new(ReplaceBoolSchemas {
97                    skip_additional_properties: true,
98                }),
99                Box::new(SetSingleExample {
100                    retain_examples: false,
101                }),
102            ],
103            inline_subschemas: false,
104        }
105    }
106
107    /// Modifies the `SchemaSettings` by calling the given function.
108    ///
109    /// # Example
110    /// ```
111    ///# extern crate apistos_schemars as schemars;
112    /// use schemars::r#gen::{SchemaGenerator, SchemaSettings};
113    ///
114    /// let settings = SchemaSettings::default().with(|s| {
115    ///     s.option_nullable = true;
116    ///     s.option_add_null_type = false;
117    /// });
118    /// let generator = settings.into_generator();
119    /// ```
120    pub fn with(mut self, configure_fn: impl FnOnce(&mut Self)) -> Self {
121        configure_fn(&mut self);
122        self
123    }
124
125    /// Appends the given visitor to the list of [visitors](SchemaSettings::visitors) for these `SchemaSettings`.
126    pub fn with_visitor(mut self, visitor: impl Visitor + Debug + Clone + 'static) -> Self {
127        self.visitors.push(Box::new(visitor));
128        self
129    }
130
131    /// Creates a new [`SchemaGenerator`] using these settings.
132    pub fn into_generator(self) -> SchemaGenerator {
133        SchemaGenerator::new(self)
134    }
135}
136
137/// The main type used to generate JSON Schemas.
138///
139/// # Example
140/// ```
141///# extern crate apistos_schemars as schemars;
142/// use schemars::{JsonSchema, r#gen::SchemaGenerator};
143///
144/// #[derive(JsonSchema)]
145/// struct MyStruct {
146///     foo: i32,
147/// }
148///
149/// let generator = SchemaGenerator::default();
150/// let schema = generator.into_root_schema_for::<MyStruct>();
151/// ```
152#[derive(Debug, Default)]
153pub struct SchemaGenerator {
154    settings: SchemaSettings,
155    definitions: Map<String, Schema>,
156    pending_schema_ids: HashSet<Cow<'static, str>>,
157    schema_id_to_name: HashMap<Cow<'static, str>, String>,
158    used_schema_names: HashSet<String>,
159}
160
161impl Clone for SchemaGenerator {
162    fn clone(&self) -> Self {
163        Self {
164            settings: self.settings.clone(),
165            definitions: self.definitions.clone(),
166            pending_schema_ids: HashSet::new(),
167            schema_id_to_name: HashMap::new(),
168            used_schema_names: HashSet::new(),
169        }
170    }
171}
172
173impl From<SchemaSettings> for SchemaGenerator {
174    fn from(settings: SchemaSettings) -> Self {
175        settings.into_generator()
176    }
177}
178
179impl SchemaGenerator {
180    /// Creates a new `SchemaGenerator` using the given settings.
181    pub fn new(settings: SchemaSettings) -> SchemaGenerator {
182        SchemaGenerator {
183            settings,
184            ..Default::default()
185        }
186    }
187
188    /// Borrows the [`SchemaSettings`] being used by this `SchemaGenerator`.
189    ///
190    /// # Example
191    /// ```
192    ///# extern crate apistos_schemars as schemars;
193    /// use schemars::r#gen::SchemaGenerator;
194    ///
195    /// let generator = SchemaGenerator::default();
196    /// let settings = generator.settings();
197    ///
198    /// assert_eq!(settings.option_add_null_type, true);
199    /// ```
200    pub fn settings(&self) -> &SchemaSettings {
201        &self.settings
202    }
203
204    #[deprecated = "This method no longer has any effect."]
205    pub fn make_extensible(&self, _schema: &mut SchemaObject) {}
206
207    #[deprecated = "Use `Schema::Bool(true)` instead"]
208    pub fn schema_for_any(&self) -> Schema {
209        Schema::Bool(true)
210    }
211
212    #[deprecated = "Use `Schema::Bool(false)` instead"]
213    pub fn schema_for_none(&self) -> Schema {
214        Schema::Bool(false)
215    }
216
217    /// Generates a JSON Schema for the type `T`, and returns either the schema itself or a `$ref` schema referencing `T`'s schema.
218    ///
219    /// If `T` is [referenceable](JsonSchema::is_referenceable), this will add `T`'s schema to this generator's definitions, and
220    /// return a `$ref` schema referencing that schema. Otherwise, this method behaves identically to [`JsonSchema::json_schema`].
221    ///
222    /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
223    /// add them to the `SchemaGenerator`'s schema definitions.
224    pub fn subschema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema {
225        let id = T::schema_id();
226        let return_ref = T::is_referenceable()
227            && (!self.settings.inline_subschemas || self.pending_schema_ids.contains(&id));
228
229        if return_ref {
230            let name = match self.schema_id_to_name.get(&id).cloned() {
231                Some(n) => n,
232                None => {
233                    let base_name = T::schema_name();
234                    let mut name = String::new();
235
236                    if self.used_schema_names.contains(&base_name) {
237                        for i in 2.. {
238                            name = format!("{}{}", base_name, i);
239                            if !self.used_schema_names.contains(&name) {
240                                break;
241                            }
242                        }
243                    } else {
244                        name = base_name;
245                    }
246
247                    self.used_schema_names.insert(name.clone());
248                    self.schema_id_to_name.insert(id.clone(), name.clone());
249                    name
250                }
251            };
252
253            let reference = format!("{}{}", self.settings.definitions_path, name);
254            if !self.definitions.contains_key(&name) {
255                self.insert_new_subschema_for::<T>(name, id);
256            }
257            Schema::new_ref(reference)
258        } else {
259            self.json_schema_internal::<T>(id)
260        }
261    }
262
263    fn insert_new_subschema_for<T: ?Sized + JsonSchema>(
264        &mut self,
265        name: String,
266        id: Cow<'static, str>,
267    ) {
268        let dummy = Schema::Bool(false);
269        // insert into definitions BEFORE calling json_schema to avoid infinite recursion
270        self.definitions.insert(name.clone(), dummy);
271
272        let schema = self.json_schema_internal::<T>(id);
273
274        self.definitions.insert(name, schema);
275    }
276
277    /// Borrows the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated.
278    ///
279    /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas
280    /// themselves.
281    pub fn definitions(&self) -> &Map<String, Schema> {
282        &self.definitions
283    }
284
285    /// Mutably borrows the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated.
286    ///
287    /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas
288    /// themselves.
289    pub fn definitions_mut(&mut self) -> &mut Map<String, Schema> {
290        &mut self.definitions
291    }
292
293    /// Returns the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated,
294    /// leaving an empty map in its place.
295    ///
296    /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas
297    /// themselves.
298    pub fn take_definitions(&mut self) -> Map<String, Schema> {
299        std::mem::take(&mut self.definitions)
300    }
301
302    /// Returns an iterator over the [visitors](SchemaSettings::visitors) being used by this `SchemaGenerator`.
303    pub fn visitors_mut(&mut self) -> impl Iterator<Item = &mut dyn GenVisitor> {
304        self.settings.visitors.iter_mut().map(|v| v.as_mut())
305    }
306
307    /// Generates a root JSON Schema for the type `T`.
308    ///
309    /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
310    /// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s
311    /// [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
312    pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
313        let mut schema = self.json_schema_internal::<T>(T::schema_id()).into_object();
314        schema.metadata().title.get_or_insert_with(T::schema_name);
315        let mut root = RootSchema {
316            meta_schema: self.settings.meta_schema.clone(),
317            definitions: self.definitions.clone(),
318            schema,
319        };
320
321        for visitor in &mut self.settings.visitors {
322            visitor.visit_root_schema(&mut root)
323        }
324
325        root
326    }
327
328    /// Consumes `self` and generates a root JSON Schema for the type `T`.
329    ///
330    /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
331    /// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
332    pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema {
333        let mut schema = self.json_schema_internal::<T>(T::schema_id()).into_object();
334        schema.metadata().title.get_or_insert_with(T::schema_name);
335        let mut root = RootSchema {
336            meta_schema: self.settings.meta_schema,
337            definitions: self.definitions,
338            schema,
339        };
340
341        for visitor in &mut self.settings.visitors {
342            visitor.visit_root_schema(&mut root)
343        }
344
345        root
346    }
347
348    /// Generates a root JSON Schema for the given example value.
349    ///
350    /// If the value implements [`JsonSchema`](crate::JsonSchema), then prefer using the [`root_schema_for()`](Self::root_schema_for())
351    /// function which will generally produce a more precise schema, particularly when the value contains any enums.
352    pub fn root_schema_for_value<T: ?Sized + Serialize>(
353        &mut self,
354        value: &T,
355    ) -> Result<RootSchema, serde_json::Error> {
356        let mut schema = value
357            .serialize(crate::ser::Serializer {
358                generator: self,
359                include_title: true,
360            })?
361            .into_object();
362
363        if let Ok(example) = serde_json::to_value(value) {
364            schema.metadata().examples.push(example);
365        }
366
367        let mut root = RootSchema {
368            meta_schema: self.settings.meta_schema.clone(),
369            definitions: self.definitions.clone(),
370            schema,
371        };
372
373        for visitor in &mut self.settings.visitors {
374            visitor.visit_root_schema(&mut root)
375        }
376
377        Ok(root)
378    }
379
380    /// Consumes `self` and generates a root JSON Schema for the given example value.
381    ///
382    /// If the value  implements [`JsonSchema`](crate::JsonSchema), then prefer using the [`into_root_schema_for()!`](Self::into_root_schema_for())
383    /// function which will generally produce a more precise schema, particularly when the value contains any enums.
384    pub fn into_root_schema_for_value<T: ?Sized + Serialize>(
385        mut self,
386        value: &T,
387    ) -> Result<RootSchema, serde_json::Error> {
388        let mut schema = value
389            .serialize(crate::ser::Serializer {
390                generator: &mut self,
391                include_title: true,
392            })?
393            .into_object();
394
395        if let Ok(example) = serde_json::to_value(value) {
396            schema.metadata().examples.push(example);
397        }
398
399        let mut root = RootSchema {
400            meta_schema: self.settings.meta_schema,
401            definitions: self.definitions,
402            schema,
403        };
404
405        for visitor in &mut self.settings.visitors {
406            visitor.visit_root_schema(&mut root)
407        }
408
409        Ok(root)
410    }
411
412    /// Attemps to find the schema that the given `schema` is referencing.
413    ///
414    /// If the given `schema` has a [`$ref`](../schema/struct.SchemaObject.html#structfield.reference) property which refers
415    /// to another schema in `self`'s schema definitions, the referenced schema will be returned. Otherwise, returns `None`.
416    ///
417    /// # Example
418    /// ```
419    ///# extern crate apistos_schemars as schemars;
420    /// use schemars::{JsonSchema, r#gen::SchemaGenerator};
421    ///
422    /// #[derive(JsonSchema)]
423    /// struct MyStruct {
424    ///     foo: i32,
425    /// }
426    ///
427    /// let mut generator = SchemaGenerator::default();
428    /// let ref_schema = generator.subschema_for::<MyStruct>();
429    ///
430    /// assert!(ref_schema.is_ref());
431    ///
432    /// let dereferenced = generator.dereference(&ref_schema);
433    ///
434    /// assert!(dereferenced.is_some());
435    /// assert!(!dereferenced.unwrap().is_ref());
436    /// assert_eq!(dereferenced, generator.definitions().get("MyStruct"));
437    /// ```
438    pub fn dereference<'a>(&'a self, schema: &Schema) -> Option<&'a Schema> {
439        match schema {
440            Schema::Object(SchemaObject {
441                reference: Some(ref schema_ref),
442                ..
443            }) => {
444                let definitions_path = &self.settings().definitions_path;
445                if schema_ref.starts_with(definitions_path) {
446                    let name = &schema_ref[definitions_path.len()..];
447                    self.definitions.get(name)
448                } else {
449                    None
450                }
451            }
452            _ => None,
453        }
454    }
455
456    fn json_schema_internal<T: ?Sized + JsonSchema>(&mut self, id: Cow<'static, str>) -> Schema {
457        struct PendingSchemaState<'a> {
458            generator: &'a mut SchemaGenerator,
459            id: Cow<'static, str>,
460            did_add: bool,
461        }
462
463        impl<'a> PendingSchemaState<'a> {
464            fn new(generator: &'a mut SchemaGenerator, id: Cow<'static, str>) -> Self {
465                let did_add = generator.pending_schema_ids.insert(id.clone());
466                Self {
467                    generator,
468                    id,
469                    did_add,
470                }
471            }
472        }
473
474        impl Drop for PendingSchemaState<'_> {
475            fn drop(&mut self) {
476                if self.did_add {
477                    self.generator.pending_schema_ids.remove(&self.id);
478                }
479            }
480        }
481
482        let pss = PendingSchemaState::new(self, id);
483        T::json_schema(pss.generator)
484    }
485}
486
487/// A [Visitor](Visitor) which implements additional traits required to be included in a [SchemaSettings].
488///
489/// You will rarely need to use this trait directly as it is automatically implemented for any type which implements all of:
490/// - [`Visitor`]
491/// - [`std::fmt::Debug`]
492/// - [`std::any::Any`] (implemented for all `'static` types)
493/// - [`std::clone::Clone`]
494///
495/// # Example
496/// ```
497///# extern crate apistos_schemars as schemars;
498/// use schemars::visit::Visitor;
499/// use schemars::r#gen::GenVisitor;
500///
501/// #[derive(Debug, Clone)]
502/// struct MyVisitor;
503///
504/// impl Visitor for MyVisitor { }
505///
506/// let v: &dyn GenVisitor = &MyVisitor;
507/// assert!(v.as_any().is::<MyVisitor>());
508/// ```
509pub trait GenVisitor: Visitor + Debug + DynClone + Any {
510    /// Upcasts this visitor into an `Any`, which can be used to inspect and manipulate it as its concrete type.
511    fn as_any(&self) -> &dyn Any;
512}
513
514dyn_clone::clone_trait_object!(GenVisitor);
515
516impl<T> GenVisitor for T
517where
518    T: Visitor + Debug + Clone + Any,
519{
520    fn as_any(&self) -> &dyn Any {
521        self
522    }
523}