Repository: pydantic/pydantic

Issue:
Recursion error when generating schema
<!-- Questions, Feature Requests, and Bug Reports are all welcome -->
<!-- delete as applicable: -->
# Bug

For bugs/questions:
* OS: **ubuntu 18.04**
* Python version `import sys; print(sys.version)`: **3.7.3**
* Pydantic version `import pydantic; print(pydantic.VERSION)`: **0.25**

Running the code below to generate schema causes a recursion error.

```py
from __future__ import annotations
import typing
from pydantic import BaseModel, Schema

class Account(BaseModel):
  name: str
  subaccounts: typing.List[Account] = []

Account.update_forward_refs()
print(Account.schema_json(indent=2))
...
```

Snippet of recursion traceback

```py
...
   flat_models |= get_flat_models_from_model(field.type_)
  File "/home/nav/.local/share/virtualenvs/project-oxBoBmI4/lib/python3.7/site-packages/pydantic/schema.py", line 355, in get_flat_models_from_model
    flat_models |= get_flat_models_from_fields(fields)
  File "/home/nav/.local/share/virtualenvs/project-oxBoBmI4/lib/python3.7/site-packages/pydantic/schema.py", line 394, in get_flat_models_from_fields
    flat_models |= get_flat_models_from_field(field)
  File "/home/nav/.local/share/virtualenvs/project-oxBoBmI4/lib/python3.7/site-packages/pydantic/schema.py", line 373, in get_flat_models_from_field
    elif lenient_issubclass(field.type_, main.BaseModel):
  File "/home/nav/.local/share/virtualenvs/project-oxBoBmI4/lib/python3.7/site-packages/pydantic/utils.py", line 232, in lenient_issubclass
    return isinstance(cls, type) and issubclass(cls, class_or_tuple)
  File "/home/nav/.local/share/virtualenvs/project-oxBoBmI4/lib/python3.7/abc.py", line 143, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
RecursionError: maximum recursion depth exceeded in comparison
```



Discussion:
I'm not sure what the correct behaviour would be here?

Since the model is self referencing, the schema surely is infinitely recursive?

@tiangolo do you have an idea about what to do here? Maybe just a more constructive error.
Yes, it should be able to create the JSON Schema, assign an ID to it and then reference it with `$ref`. Here's the relevant section in the spec: http://json-schema.org/latest/json-schema-core.html#rfc.section.8.3

I'll try to fix it as soon as I get a chance.
Awesome, thank you. I think you're the best person to do it since you understand schema best.

Resolving patch:
diff --git a/pydantic/schema.py b/pydantic/schema.py
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -220,9 +220,14 @@ def model_schema(model: Type['BaseModel'], by_alias: bool = True, ref_prefix: Op
     ref_prefix = ref_prefix or default_prefix
     flat_models = get_flat_models_from_model(model)
     model_name_map = get_model_name_map(flat_models)
+    model_name = model_name_map[model]
     m_schema, m_definitions = model_process_schema(
         model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
     )
+    if model_name in m_definitions:
+        # m_definitions[model_name] is None, it has circular references
+        m_definitions[model_name] = m_schema
+        m_schema = {'$ref': ref_prefix + model_name}
     if m_definitions:
         m_schema.update({'definitions': m_definitions})
     return m_schema
@@ -234,6 +239,7 @@ def field_schema(
     by_alias: bool = True,
     model_name_map: Dict[Type['BaseModel'], str],
     ref_prefix: Optional[str] = None,
+    known_models: Set[Type['BaseModel']] = None,
 ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
     """
     Process a Pydantic field and return a tuple with a JSON Schema for it as the first item.
@@ -246,6 +252,7 @@ def field_schema(
     :param model_name_map: used to generate the JSON Schema references to other models included in the definitions
     :param ref_prefix: the JSON Pointer prefix to use for references to other schemas, if None, the default of
       #/definitions/ will be used
+    :param known_models: used to solve circular references
     :return: tuple of the schema for this field and additional definitions
     """
     ref_prefix = ref_prefix or default_prefix
@@ -274,6 +281,7 @@ def field_schema(
         model_name_map=model_name_map,
         schema_overrides=schema_overrides,
         ref_prefix=ref_prefix,
+        known_models=known_models or set(),
     )
     # $ref will only be returned when there are no schema_overrides
     if '$ref' in f_schema:
@@ -350,7 +358,9 @@ def get_model_name_map(unique_models: Set[Type['BaseModel']]) -> Dict[Type['Base
     return {v: k for k, v in name_model_map.items()}
 
 
-def get_flat_models_from_model(model: Type['BaseModel']) -> Set[Type['BaseModel']]:
+def get_flat_models_from_model(
+    model: Type['BaseModel'], known_models: Set[Type['BaseModel']] = None
+) -> Set[Type['BaseModel']]:
     """
     Take a single ``model`` and generate a set with itself and all the sub-models in the tree. I.e. if you pass
     model ``Foo`` (subclass of Pydantic ``BaseModel``) as ``model``, and it has a field of type ``Bar`` (also
@@ -358,16 +368,19 @@ def get_flat_models_from_model(model: Type['BaseModel']) -> Set[Type['BaseModel'
     the return value will be ``set([Foo, Bar, Baz])``.
 
     :param model: a Pydantic ``BaseModel`` subclass
+    :param known_models: used to solve circular references
     :return: a set with the initial model and all its sub-models
     """
+    known_models = known_models or set()
     flat_models: Set[Type['BaseModel']] = set()
     flat_models.add(model)
+    known_models |= flat_models
     fields = cast(Sequence[Field], model.__fields__.values())
-    flat_models |= get_flat_models_from_fields(fields)
+    flat_models |= get_flat_models_from_fields(fields, known_models=known_models)
     return flat_models
 
 
-def get_flat_models_from_field(field: Field) -> Set[Type['BaseModel']]:
+def get_flat_models_from_field(field: Field, known_models: Set[Type['BaseModel']]) -> Set[Type['BaseModel']]:
     """
     Take a single Pydantic ``Field`` (from a model) that could have been declared as a sublcass of BaseModel
     (so, it could be a submodel), and generate a set with its model and all the sub-models in the tree.
@@ -376,20 +389,24 @@ def get_flat_models_from_field(field: Field) -> Set[Type['BaseModel']]:
     type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``.
 
     :param field: a Pydantic ``Field``
+    :param known_models: used to solve circular references
     :return: a set with the model used in the declaration for this field, if any, and all its sub-models
     """
     flat_models: Set[Type['BaseModel']] = set()
+    # Handle dataclass-based models
+    field_type = field.type_
+    if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), pydantic.BaseModel):
+        field_type = field_type.__pydantic_model__  # type: ignore
     if field.sub_fields:
-        flat_models |= get_flat_models_from_fields(field.sub_fields)
-    elif lenient_issubclass(field.type_, pydantic.BaseModel):
-        flat_models |= get_flat_models_from_model(field.type_)
-    elif lenient_issubclass(getattr(field.type_, '__pydantic_model__', None), pydantic.BaseModel):
-        field.type_ = cast(Type['dataclasses.DataclassType'], field.type_)
-        flat_models |= get_flat_models_from_model(field.type_.__pydantic_model__)
+        flat_models |= get_flat_models_from_fields(field.sub_fields, known_models=known_models)
+    elif lenient_issubclass(field_type, pydantic.BaseModel) and field_type not in known_models:
+        flat_models |= get_flat_models_from_model(field_type, known_models=known_models)
     return flat_models
 
 
-def get_flat_models_from_fields(fields: Sequence[Field]) -> Set[Type['BaseModel']]:
+def get_flat_models_from_fields(
+    fields: Sequence[Field], known_models: Set[Type['BaseModel']]
+) -> Set[Type['BaseModel']]:
     """
     Take a list of Pydantic  ``Field``s (from a model) that could have been declared as sublcasses of ``BaseModel``
     (so, any of them could be a submodel), and generate a set with their models and all the sub-models in the tree.
@@ -398,11 +415,12 @@ def get_flat_models_from_fields(fields: Sequence[Field]) -> Set[Type['BaseModel'
     subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``.
 
     :param fields: a list of Pydantic ``Field``s
+    :param known_models: used to solve circular references
     :return: a set with any model declared in the fields, and all their sub-models
     """
     flat_models: Set[Type['BaseModel']] = set()
     for field in fields:
-        flat_models |= get_flat_models_from_field(field)
+        flat_models |= get_flat_models_from_field(field, known_models=known_models)
     return flat_models
 
 
@@ -429,6 +447,7 @@ def field_type_schema(
     model_name_map: Dict[Type['BaseModel'], str],
     schema_overrides: bool = False,
     ref_prefix: Optional[str] = None,
+    known_models: Set[Type['BaseModel']],
 ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
     """
     Used by ``field_schema()``, you probably should be using that function.
@@ -440,13 +459,13 @@ def field_type_schema(
     ref_prefix = ref_prefix or default_prefix
     if field.shape is Shape.LIST:
         f_schema, f_definitions = field_singleton_schema(
-            field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
+            field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
         )
         definitions.update(f_definitions)
         return {'type': 'array', 'items': f_schema}, definitions
     elif field.shape is Shape.SET:
         f_schema, f_definitions = field_singleton_schema(
-            field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
+            field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
         )
         definitions.update(f_definitions)
         return {'type': 'array', 'uniqueItems': True, 'items': f_schema}, definitions
@@ -455,7 +474,7 @@ def field_type_schema(
         key_field = cast(Field, field.key_field)
         regex = getattr(key_field.type_, 'regex', None)
         f_schema, f_definitions = field_singleton_schema(
-            field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
+            field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
         )
         definitions.update(f_definitions)
         if regex:
@@ -471,7 +490,7 @@ def field_type_schema(
         sub_fields = cast(List[Field], field.sub_fields)
         for sf in sub_fields:
             sf_schema, sf_definitions = field_type_schema(
-                sf, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
+                sf, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
             )
             definitions.update(sf_definitions)
             sub_schema.append(sf_schema)
@@ -486,6 +505,7 @@ def field_type_schema(
             model_name_map=model_name_map,
             schema_overrides=schema_overrides,
             ref_prefix=ref_prefix,
+            known_models=known_models,
         )
         definitions.update(f_definitions)
         return f_schema, definitions
@@ -497,6 +517,7 @@ def model_process_schema(
     by_alias: bool = True,
     model_name_map: Dict[Type['BaseModel'], str],
     ref_prefix: Optional[str] = None,
+    known_models: Set[Type['BaseModel']] = None,
 ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
     """
     Used by ``model_schema()``, you probably should be using that function.
@@ -506,11 +527,13 @@ def model_process_schema(
     the definitions are returned as the second value.
     """
     ref_prefix = ref_prefix or default_prefix
+    known_models = known_models or set()
     s = {'title': model.__config__.title or model.__name__}
     if model.__doc__:
         s['description'] = clean_docstring(model.__doc__)
+    known_models.add(model)
     m_schema, m_definitions = model_type_schema(
-        model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
+        model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
     )
     s.update(m_schema)
     return s, m_definitions
@@ -522,6 +545,7 @@ def model_type_schema(
     by_alias: bool,
     model_name_map: Dict[Type['BaseModel'], str],
     ref_prefix: Optional[str] = None,
+    known_models: Set[Type['BaseModel']],
 ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
     """
     You probably should be using ``model_schema()``, this function is indirectly used by that function.
@@ -536,7 +560,7 @@ def model_type_schema(
     for k, f in model.__fields__.items():
         try:
             f_schema, f_definitions = field_schema(
-                f, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
+                f, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
             )
         except SkipField as skip:
             warnings.warn(skip.message, UserWarning)
@@ -563,6 +587,7 @@ def field_singleton_sub_fields_schema(
     model_name_map: Dict[Type['BaseModel'], str],
     schema_overrides: bool = False,
     ref_prefix: Optional[str] = None,
+    known_models: Set[Type['BaseModel']],
 ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
     """
     This function is indirectly used by ``field_schema()``, you probably should be using that function.
@@ -580,6 +605,7 @@ def field_singleton_sub_fields_schema(
             model_name_map=model_name_map,
             schema_overrides=schema_overrides,
             ref_prefix=ref_prefix,
+            known_models=known_models,
         )
     else:
         sub_field_schemas = []
@@ -590,6 +616,7 @@ def field_singleton_sub_fields_schema(
                 model_name_map=model_name_map,
                 schema_overrides=schema_overrides,
                 ref_prefix=ref_prefix,
+                known_models=known_models,
             )
             definitions.update(sub_definitions)
             sub_field_schemas.append(sub_schema)
@@ -662,6 +689,7 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
     model_name_map: Dict[Type['BaseModel'], str],
     schema_overrides: bool = False,
     ref_prefix: Optional[str] = None,
+    known_models: Set[Type['BaseModel']],
 ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
     """
     This function is indirectly used by ``field_schema()``, you should probably be using that function.
@@ -678,6 +706,7 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
             model_name_map=model_name_map,
             schema_overrides=schema_overrides,
             ref_prefix=ref_prefix,
+            known_models=known_models,
         )
     if field.type_ is Any:
         return {}, definitions  # no restrictions
@@ -708,19 +737,26 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
     # Handle dataclass-based models
     field_type = field.type_
     if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), pydantic.BaseModel):
-        field_type = cast(Type['dataclasses.DataclassType'], field_type)
-        field_type = field_type.__pydantic_model__
+        field_type = field_type.__pydantic_model__  # type: ignore
     if issubclass(field_type, pydantic.BaseModel):
-        sub_schema, sub_definitions = model_process_schema(
-            field_type, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
-        )
-        definitions.update(sub_definitions)
-        if not schema_overrides:
-            model_name = model_name_map[field_type]
+        model_name = model_name_map[field_type]
+        if field_type not in known_models:
+            sub_schema, sub_definitions = model_process_schema(
+                field_type,
+                by_alias=by_alias,
+                model_name_map=model_name_map,
+                ref_prefix=ref_prefix,
+                known_models=known_models,
+            )
+            definitions.update(sub_definitions)
             definitions[model_name] = sub_schema
-            return {'$ref': f'{ref_prefix}{model_name}'}, definitions
         else:
-            return sub_schema, definitions
+            definitions[model_name] = None
+        schema_ref = {'$ref': ref_prefix + model_name}
+        if not schema_overrides:
+            return schema_ref, definitions
+        else:
+            return {'allOf': [schema_ref]}, definitions
     raise ValueError(f'Value not declarable with JSON Schema, field: {field}')
 
 


Test patch:
diff --git a/tests/test_py37.py b/tests/test_py37.py
--- a/tests/test_py37.py
+++ b/tests/test_py37.py
@@ -203,7 +203,7 @@ class Node(BaseModel):
     )
     Node = module.Node
     Leaf = module.Leaf
-    data = {"value": 3, "left": {"a": "foo"}, "right": {"value": 5, "left": {"a": "bar"}, "right": {"a": "buzz"}}}
+    data = {'value': 3, 'left': {'a': 'foo'}, 'right': {'value': 5, 'left': {'a': 'bar'}, 'right': {'a': 'buzz'}}}
 
     node = Node(**data)
     assert isinstance(node.left, Leaf)
@@ -238,11 +238,97 @@ class Node(BaseModel):
     Node = module.Node
     Leaf = module.Leaf
     data = {
-        "value": 3,
-        "left": {"a": "foo"},
-        "right": [{"value": 5, "left": {"a": "bar"}, "right": {"a": "buzz"}}, "test"],
+        'value': 3,
+        'left': {'a': 'foo'},
+        'right': [{'value': 5, 'left': {'a': 'bar'}, 'right': {'a': 'buzz'}}, 'test'],
     }
 
     node = Node(**data)
     assert isinstance(node.left, Leaf)
     assert isinstance(node.right[0], Node)
+
+
+@skip_not_37
+def test_self_reference_json_schema(create_module):
+    module = create_module(
+        """
+from __future__ import annotations
+from typing import List
+from pydantic import BaseModel, Schema
+
+class Account(BaseModel):
+  name: str
+  subaccounts: List[Account] = []
+
+Account.update_forward_refs()
+    """
+    )
+    Account = module.Account
+    assert Account.schema() == {
+        '$ref': '#/definitions/Account',
+        'definitions': {
+            'Account': {
+                'title': 'Account',
+                'type': 'object',
+                'properties': {
+                    'name': {'title': 'Name', 'type': 'string'},
+                    'subaccounts': {
+                        'title': 'Subaccounts',
+                        'default': [],
+                        'type': 'array',
+                        'items': {'$ref': '#/definitions/Account'},
+                    },
+                },
+                'required': ['name'],
+            }
+        },
+    }
+
+
+@skip_not_37
+def test_circular_reference_json_schema(create_module):
+    module = create_module(
+        """
+from __future__ import annotations
+from typing import List
+from pydantic import BaseModel, Schema
+
+class Owner(BaseModel):
+  account: Account
+
+class Account(BaseModel):
+  name: str
+  owner: Owner
+  subaccounts: List[Account] = []
+
+Account.update_forward_refs()
+Owner.update_forward_refs()
+    """
+    )
+    Account = module.Account
+    assert Account.schema() == {
+        '$ref': '#/definitions/Account',
+        'definitions': {
+            'Account': {
+                'title': 'Account',
+                'type': 'object',
+                'properties': {
+                    'name': {'title': 'Name', 'type': 'string'},
+                    'owner': {'$ref': '#/definitions/Owner'},
+                    'subaccounts': {
+                        'title': 'Subaccounts',
+                        'default': [],
+                        'type': 'array',
+                        'items': {'$ref': '#/definitions/Account'},
+                    },
+                },
+                'required': ['name', 'owner'],
+            },
+            'Owner': {
+                'title': 'Owner',
+                'type': 'object',
+                'properties': {'account': {'$ref': '#/definitions/Account'}},
+                'required': ['account'],
+            },
+        },
+    }
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -917,18 +917,16 @@ class Model(BaseModel):
         'title': 'Model',
         'type': 'object',
         'definitions': {
+            'Foo': {
+                'title': 'Foo',
+                'type': 'object',
+                'properties': {'a': {'title': 'A', 'type': 'string'}},
+                'required': ['a'],
+            },
             'Bar': {
                 'title': 'Bar',
                 'type': 'object',
-                'properties': {
-                    'b': {
-                        'title': 'Foo',
-                        'type': 'object',
-                        'properties': {'a': {'title': 'A', 'type': 'string'}},
-                        'required': ['a'],
-                        'default': {'a': 'foo'},
-                    }
-                },
+                'properties': {'b': {'title': 'B', 'default': {'a': 'foo'}, 'allOf': [{'$ref': '#/definitions/Foo'}]}},
             },
             'Baz': {'title': 'Baz', 'type': 'object', 'properties': {'c': {'$ref': '#/definitions/Bar'}}},
         },