Repository: pydantic/pydantic
Pull request: Add support for JSON Schema with circular references in Python 3.7
<!-- Thank you for your contribution! -->
<!-- See https://pydantic-docs.helpmanual.io/#contributing-to-pydantic for help on Contributing -->
<!-- Don't worry about making lots of commits on a pull request, they'll be squashed on merge anyway -->

## Change Summary

Add JSON Schema generation support for models with circular references, including self-referencing.

<!-- Please give a short summary of the changes. -->

## Related issue number

This fixes #531, the comment in https://github.com/samuelcolvin/pydantic/issues/524#issuecomment-497749167.

I think parts of it might move in the direction of #478 too.

<!-- Are there any issues opened that will be resolved by merging this change? -->

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `HISTORY.rst` has been updated
  * if this is the first change since a release, please add a new section
  * include the issue number or this pull request number `#<number>`
  * include your github username `@<whomever>`


Problem statement:
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
```

Hints from issue 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.

Commit message: Trigger Travis and others

Patch to review:
diff --git a/HISTORY.rst b/HISTORY.rst
index eb210788ca8..81214409896 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,10 @@
 History
 -------
 
+v0.xx (xxxx-xx-xx)
+..................
+* fix support for JSON Schema generation when using models with circular references in Python 3.7, #572 by @tiangolo
+
 v0.27 (2019-05-30)
 ..................
 * Change ``_pydantic_post_init`` to execute dataclass' original ``__post_init__`` before
diff --git a/pydantic/schema.py b/pydantic/schema.py
index fb1603c4893..372b7951222 100644
--- 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': f'{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,9 +252,11 @@ 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
+    known_models = known_models or set()
     schema_overrides = False
     schema = cast('Schema', field.schema)
     s = dict(title=schema.title or field.alias.title())
@@ -274,6 +282,7 @@ def field_schema(
         model_name_map=model_name_map,
         schema_overrides=schema_overrides,
         ref_prefix=ref_prefix,
+        known_models=known_models,
     )
     # $ref will only be returned when there are no schema_overrides
     if '$ref' in f_schema:
@@ -350,7 +359,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 +369,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 +390,25 @@ 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 = cast(Type['dataclasses.DataclassType'], field_type)
+        field_type = field_type.__pydantic_model__
     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 +417,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 +449,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 +461,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 +476,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 +492,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 +507,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 +519,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 +529,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 +547,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 +562,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 +589,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 +607,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 +618,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 +691,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 +708,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
@@ -711,16 +742,24 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
         field_type = cast(Type['dataclasses.DataclassType'], field_type)
         field_type = field_type.__pydantic_model__
     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': f'{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}')
 
 
diff --git a/tests/test_py37.py b/tests/test_py37.py
index a1b17a3c608..15f53d7992a 100644
--- a/tests/test_py37.py
+++ b/tests/test_py37.py
@@ -246,3 +246,89 @@ class Node(BaseModel):
     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
index 78976a6d5bc..6a368682e2d 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -916,24 +916,22 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
+        'properties': {'d': {'$ref': '#/definitions/Baz'}},
+        'required': ['d'],
         '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'}}},
         },
-        'properties': {'d': {'$ref': '#/definitions/Baz'}},
-        'required': ['d'],
     }
 
 


Code review comments:
diff_hunk: @@ -376,20 +390,25 @@ 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 = cast(Type['dataclasses.DataclassType'], field_type)
path: pydantic/schema.py
text: @user1:
`cast` seems to be weirdly, if you can use type hints and `# type: ignore`, it would be better.

diff_hunk: @@ -916,24 +916,22 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
+        'properties': {'d': {'$ref': '#/definitions/Baz'}},
path: tests/test_schema.py
text: @user1:
isn't this just moved around? If so, could you move it back?

@author:
Yep. Copy-paste conflict. Fixed.

diff_hunk: @@ -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': f'{ref_prefix}{model_name}'}
path: pydantic/schema.py
text: @user1:
```suggestion
        m_schema = {'$ref': ref_prefix + model_name}
```

Cleaner in this case unless either aren't always strings.

diff_hunk: @@ -711,16 +742,24 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
         field_type = cast(Type['dataclasses.DataclassType'], field_type)
         field_type = field_type.__pydantic_model__
     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': f'{ref_prefix}{model_name}'}
path: pydantic/schema.py
text: @user1:
```suggestion
        schema_ref = {'$ref': ref_prefix + model_name}
```

diff_hunk: @@ -274,6 +282,7 @@ def field_schema(
         model_name_map=model_name_map,
         schema_overrides=schema_overrides,
         ref_prefix=ref_prefix,
+        known_models=known_models,
path: pydantic/schema.py
text: @user1:
```suggestion
        known_models=known_models or set(),
```

and the line above can be removed.

@author:
Wouldn't this possibly lead to issues with "Mutable Default Arguments"? https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments

@user1:
no, this is calling the function not defining it I think.

@author:
Ah! Right. I thought this comment was in the definition.

diff_hunk: @@ -376,20 +390,25 @@ 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 = cast(Type['dataclasses.DataclassType'], field_type)
path: pydantic/schema.py
text: @user1:
`cast` seems to be weirdly, if you can use type hints and `# type: ignore`, it would be better.