Skip to content

Commit

Permalink
Prevent stack overflow when using inline_subschemas
Browse files Browse the repository at this point in the history
  • Loading branch information
GREsau committed Mar 21, 2021
1 parent d85eec3 commit 1017506
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 30 deletions.
56 changes: 41 additions & 15 deletions schemars/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::flatten::Merge;
use crate::schema::*;
use crate::{visit::*, JsonSchema, Map};
use dyn_clone::DynClone;
use std::{any::Any, fmt::Debug};
use std::{any::Any, collections::HashSet, fmt::Debug};

/// Settings to customize how Schemas are generated.
///
Expand Down Expand Up @@ -146,10 +146,21 @@ impl SchemaSettings {
/// let gen = SchemaGenerator::default();
/// let schema = gen.into_root_schema_for::<MyStruct>();
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default)]
pub struct SchemaGenerator {
settings: SchemaSettings,
definitions: Map<String, Schema>,
pending_schema_names: HashSet<String>,
}

impl Clone for SchemaGenerator {
fn clone(&self) -> Self {
Self {
settings: self.settings.clone(),
definitions: self.definitions.clone(),
pending_schema_names: HashSet::new(),
}
}
}

impl From<SchemaSettings> for SchemaGenerator {
Expand Down Expand Up @@ -203,23 +214,28 @@ impl SchemaGenerator {
/// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
/// add them to the `SchemaGenerator`'s schema definitions.
pub fn subschema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema {
if !T::is_referenceable() || self.settings.inline_subschemas {
return T::json_schema(self);
}

let name = T::schema_name();
let reference = format!("{}{}", self.settings().definitions_path, name);
if !self.definitions.contains_key(&name) {
self.insert_new_subschema_for::<T>(name);
let return_ref = T::is_referenceable()
&& (!self.settings.inline_subschemas || self.pending_schema_names.contains(&name));

if return_ref {
let reference = format!("{}{}", self.settings().definitions_path, name);
if !self.definitions.contains_key(&name) {
self.insert_new_subschema_for::<T>(name);
}
Schema::new_ref(reference)
} else {
self.json_schema_internal::<T>(&name)
}
Schema::new_ref(reference)
}

fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: String) {
let dummy = Schema::Bool(false);
// insert into definitions BEFORE calling json_schema to avoid infinite recursion
self.definitions.insert(name.clone(), dummy);
let schema = T::json_schema(self);

let schema = self.json_schema_internal::<T>(&name);

self.definitions.insert(name, schema);
}

Expand Down Expand Up @@ -259,8 +275,9 @@ impl SchemaGenerator {
/// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s
/// [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
let mut schema = T::json_schema(self).into_object();
schema.metadata().title.get_or_insert_with(T::schema_name);
let name = T::schema_name();
let mut schema = self.json_schema_internal::<T>(&name).into_object();
schema.metadata().title.get_or_insert(name);
let mut root = RootSchema {
meta_schema: self.settings.meta_schema.clone(),
definitions: self.definitions.clone(),
Expand All @@ -279,8 +296,9 @@ impl SchemaGenerator {
/// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
/// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema {
let mut schema = T::json_schema(&mut self).into_object();
schema.metadata().title.get_or_insert_with(T::schema_name);
let name = T::schema_name();
let mut schema = self.json_schema_internal::<T>(&name).into_object();
schema.metadata().title.get_or_insert(name);
let mut root = RootSchema {
meta_schema: self.settings.meta_schema,
definitions: self.definitions,
Expand Down Expand Up @@ -352,6 +370,14 @@ impl SchemaGenerator {
}
}
}

fn json_schema_internal<T: ?Sized + JsonSchema>(&mut self, name: &str) -> Schema {
self.pending_schema_names.insert(name.to_owned());
let schema = T::json_schema(self);
// FIXME schema name not removed if previous line panics
self.pending_schema_names.remove(name);
schema
}
}

/// A [Visitor](Visitor) which implements additional traits required to be included in a [SchemaSettings].
Expand Down
95 changes: 95 additions & 0 deletions schemars/tests/expected/inline-subschemas-recursive.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "RecursiveOuter",
"type": "object",
"properties": {
"direct": {
"anyOf": [
{
"$ref": "#/definitions/RecursiveOuter"
},
{
"type": "null"
}
]
},
"indirect": {
"type": [
"object",
"null"
],
"required": [
"recursive"
],
"properties": {
"recursive": {
"type": "object",
"properties": {
"direct": {
"anyOf": [
{
"$ref": "#/definitions/RecursiveOuter"
},
{
"type": "null"
}
]
},
"indirect": {
"anyOf": [
{
"$ref": "#/definitions/RecursiveInner"
},
{
"type": "null"
}
]
}
}
}
}
}
},
"definitions": {
"RecursiveOuter": {
"type": "object",
"properties": {
"direct": {
"anyOf": [
{
"$ref": "#/definitions/RecursiveOuter"
},
{
"type": "null"
}
]
},
"indirect": {
"type": [
"object",
"null"
],
"required": [
"recursive"
],
"properties": {
"recursive": {
"$ref": "#/definitions/RecursiveOuter"
}
}
}
}
},
"RecursiveInner": {
"type": "object",
"required": [
"recursive"
],
"properties": {
"recursive": {
"$ref": "#/definitions/RecursiveOuter"
}
}
}
}
}
28 changes: 14 additions & 14 deletions schemars/tests/expected/inline-subschemas.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MyJob",
"type": "object",
"required": [
"spec"
],
"properties": {
"spec": {
"type": "object",
"required": [
"replicas"
],
"properties": {
"replicas": {
"type": "integer",
"format": "uint32",
"minimum": 0,
"type": "integer"
"minimum": 0.0
}
},
"required": [
"replicas"
],
"type": "object"
}
}
},
"required": [
"spec"
],
"title": "MyJob",
"type": "object"
}
}
20 changes: 19 additions & 1 deletion schemars/tests/inline_subschemas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,25 @@ pub struct MyJobSpec {

#[test]
fn struct_normal() -> TestResult {
let mut settings = SchemaSettings::openapi3();
let mut settings = SchemaSettings::default();
settings.inline_subschemas = true;
test_generated_schema::<MyJob>("inline-subschemas", settings)
}

#[derive(Debug, JsonSchema)]
pub struct RecursiveOuter {
pub direct: Option<Box<RecursiveOuter>>,
pub indirect: Option<Box<RecursiveInner>>,
}

#[derive(Debug, JsonSchema)]
pub struct RecursiveInner {
pub recursive: RecursiveOuter,
}

#[test]
fn struct_recursive() -> TestResult {
let mut settings = SchemaSettings::default();
settings.inline_subschemas = true;
test_generated_schema::<RecursiveOuter>("inline-subschemas-recursive", settings)
}

0 comments on commit 1017506

Please sign in to comment.