Repository: vitalik/django-ninja

Issue:
API documentation generation for schema with nested "create_schema" fails
The following schema:
```python
class BlogSchema(Schema):
    id: int
    slug: str
    posts: list[
        create_schema(
            BlogPost,
            fields=['id', 'contents'],
            custom_fields=[('author', create_schema(User, fields=['id', 'full_name']), ...)],
        )
    ]
```
fails with:
```python
Internal Server Error: /api/openapi.json
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/ninja/openapi/views.py", line 25, in openapi_json
    schema = api.get_openapi_schema()
  File "/usr/local/lib/python3.9/site-packages/ninja/main.py", line 338, in get_openapi_schema
    return get_schema(api=self, path_prefix=path_prefix)
  File "/usr/local/lib/python3.9/site-packages/ninja/openapi/schema.py", line 21, in get_schema
    openapi = OpenAPISchema(api, path_prefix)
  File "/usr/local/lib/python3.9/site-packages/ninja/openapi/schema.py", line 43, in __init__
    ("paths", self.get_paths()),
  File "/usr/local/lib/python3.9/site-packages/ninja/openapi/schema.py", line 55, in get_paths
    path_methods = self.methods(path_view.operations)
  File "/usr/local/lib/python3.9/site-packages/ninja/openapi/schema.py", line 66, in methods
    result[method.lower()] = self.operation_details(op)
  File "/usr/local/lib/python3.9/site-packages/ninja/openapi/schema.py", line 80, in operation_details
    "responses": self.responses(operation),
  File "/usr/local/lib/python3.9/site-packages/ninja/openapi/schema.py", line 185, in responses
    schema, _ = self._create_schema_from_model(model, by_alias=False)
  File "/usr/local/lib/python3.9/site-packages/ninja/openapi/schema.py", line 134, in _create_schema_from_model
    schema = model_schema(model, ref_prefix=REF_PREFIX, by_alias=by_alias)
  File "pydantic/schema.py", line 167, in pydantic.schema.model_schema
  File "pydantic/schema.py", line 548, in pydantic.schema.model_process_schema
  File "pydantic/schema.py", line 589, in pydantic.schema.model_type_schema
  File "pydantic/schema.py", line 241, in pydantic.schema.field_schema
  File "pydantic/schema.py", line 495, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 842, in pydantic.schema.field_singleton_schema
  File "pydantic/schema.py", line 548, in pydantic.schema.model_process_schema
  File "pydantic/schema.py", line 589, in pydantic.schema.model_type_schema
  File "pydantic/schema.py", line 241, in pydantic.schema.field_schema
  File "pydantic/schema.py", line 440, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 773, in pydantic.schema.field_singleton_schema
  File "pydantic/schema.py", line 667, in pydantic.schema.field_singleton_sub_fields_schema
  File "pydantic/schema.py", line 495, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 842, in pydantic.schema.field_singleton_schema
  File "pydantic/schema.py", line 548, in pydantic.schema.model_process_schema
  File "pydantic/schema.py", line 589, in pydantic.schema.model_type_schema
  File "pydantic/schema.py", line 241, in pydantic.schema.field_schema
  File "pydantic/schema.py", line 440, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 773, in pydantic.schema.field_singleton_schema
  File "pydantic/schema.py", line 667, in pydantic.schema.field_singleton_sub_fields_schema
  File "pydantic/schema.py", line 495, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 842, in pydantic.schema.field_singleton_schema
  File "pydantic/schema.py", line 548, in pydantic.schema.model_process_schema
  File "pydantic/schema.py", line 589, in pydantic.schema.model_type_schema
  File "pydantic/schema.py", line 241, in pydantic.schema.field_schema
  File "pydantic/schema.py", line 495, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 840, in pydantic.schema.field_singleton_schema
KeyError: <class 'pydantic.main.Author'>
```

Discussion:

Patch:
diff --git a/ninja/openapi/schema.py b/ninja/openapi/schema.py
index 32c47ae..3641e8d 100644
--- a/ninja/openapi/schema.py
+++ b/ninja/openapi/schema.py
@@ -4,7 +4,7 @@ from http.client import responses
 from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Set, Tuple, Type
 
 from pydantic import BaseModel
-from pydantic.schema import model_schema
+from pydantic.schema import model_schema as pydantic_model_schema
 
 from ninja.constants import NOT_SET
 from ninja.operation import Operation
@@ -19,6 +19,17 @@ REF_PREFIX: str = "#/components/schemas/"
 BODY_PARAMS: Set[str] = {"body", "form", "file"}
 
 
+def model_schema(*args: Any, **kwargs: Any) -> Dict[str, Any]:
+    """trap and report some errors in pydantic schema generation"""
+    try:
+        return pydantic_model_schema(*args, **kwargs)
+    except KeyError as exc:
+        from ninja.orm.factory import factory
+
+        factory.check_for_duplicates_on_exception(exc)
+        raise
+
+
 def get_schema(api: "NinjaAPI", path_prefix: str = "") -> "OpenAPISchema":
     openapi = OpenAPISchema(api, path_prefix)
     return openapi
diff --git a/ninja/orm/factory.py b/ninja/orm/factory.py
index 49b8ff9..41c97f3 100644
--- a/ninja/orm/factory.py
+++ b/ninja/orm/factory.py
@@ -110,6 +110,22 @@ class SchemaFactory:
                 continue
             yield cast(Field, fld)
 
+    def check_for_duplicates_on_exception(self, exc: Exception) -> None:
+        """check for duplicate named schemas: https://github.com/vitalik/django-ninja/issues/214"""
+        exc_args = getattr(exc, "args", None)
+        if exc_args and isinstance(exc_args[0], type(Schema)):
+            schema = exc_args[0]
+            schema_found = tuple(k for k, v in self.schemas.items() if v == schema)
+            if schema_found:
+                model_name = schema_found[0][1]
+                same_name_keys = tuple(
+                    key for key in factory.schemas if key[1] == model_name
+                )
+                if len(same_name_keys) > 1:
+                    errors = "\n".join(f"  {key}" for key in same_name_keys)
+                    msg = f"Looks like you may have created multiple orm schemas with the same name:\n{errors}"
+                    raise ConfigError(msg) from exc
+
 
 factory = SchemaFactory()
 


Test patch:
diff --git a/tests/test_orm_schemas.py b/tests/test_orm_schemas.py
index 1236f7c..c72b96b 100644
--- a/tests/test_orm_schemas.py
+++ b/tests/test_orm_schemas.py
@@ -1,5 +1,5 @@
 from typing import List
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
 
 import django
 import pytest
@@ -474,3 +474,83 @@ def test_custom_fields():
         },
         "required": ["f1", "f2"],
     }
+
+
+def test_duplicate_schema_names():
+    from django.db import models
+
+    from ninja import NinjaAPI, Schema
+    from ninja.orm import create_schema
+
+    class TestModelDuplicate(models.Model):
+        field1 = models.CharField()
+        field2 = models.CharField()
+
+        class Meta:
+            app_label = "tests"
+
+    class TestSchema(Schema):
+        data1: create_schema(TestModelDuplicate, fields=["field1"])  # noqa: F821
+        data2: create_schema(TestModelDuplicate, fields=["field2"])  # noqa: F821
+
+    api = NinjaAPI()
+
+    @api.get("/test", response=TestSchema)
+    def a_test_method(request):
+        return []
+
+    match = r"Looks like you may have created multiple orm schemas with the same name:"
+    with pytest.raises(ConfigError, match=match):
+        assert api.get_openapi_schema()
+
+
+def test_not_duplicate_schema_names():
+    from ninja.openapi.schema import model_schema
+    from ninja.schema import Schema
+
+    class TestModelNotDuplicate(models.Model):
+        charfield = models.CharField()
+
+        class Meta:
+            app_label = "tests"
+
+    TestSchema = create_schema(
+        TestModelNotDuplicate, name="TestModelNotDuplicate", fields=["charfield"]
+    )
+
+    with patch("ninja.openapi.schema.pydantic_model_schema", side_effect=ValueError):
+        with pytest.raises(ValueError):
+            assert model_schema() is None
+
+    with patch("ninja.openapi.schema.pydantic_model_schema", side_effect=KeyError):
+        with pytest.raises(KeyError):
+            assert model_schema() is None
+
+    with patch(
+        "ninja.openapi.schema.pydantic_model_schema", side_effect=KeyError(Schema)
+    ):
+        with pytest.raises(KeyError):
+            assert model_schema() is None
+
+    with patch(
+        "ninja.openapi.schema.pydantic_model_schema", side_effect=KeyError(TestSchema)
+    ):
+        with pytest.raises(KeyError):
+            assert model_schema() is None
+
+    # define a duplicate schema name
+    create_schema(
+        TestModelNotDuplicate,
+        name="TestModelNotDuplicate",
+        fields=["charfield"],
+        custom_fields=[
+            ("test", TestSchema, ...)
+        ],  # interlinking models with the same name
+    )
+
+    # duplicate orm schema names returns ConfigError
+    with patch(
+        "ninja.openapi.schema.pydantic_model_schema", side_effect=KeyError(TestSchema)
+    ):
+        with pytest.raises(ConfigError):
+            assert model_schema() is None