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}