vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.03k stars 421 forks source link

[BUG] Self-Referencing Schemas are Broken #1198

Open akkim2 opened 3 months ago

akkim2 commented 3 months ago

Self-referencing schemas seem to be broken when following the Django Ninja documentation, producing incorrect and unusable OpenAPI schema and endpoints.

For example, using the following models:

class SelfReferenceModel(models.Model):
    parent = models.ForeignKey(to="self", on_delete=models.CASCADE, null=True, blank=True)
    text = models.CharField(max_length=500)

class SelfReferenceMainModel(models.Model):
    self_reference = models.ForeignKey(to=SelfReferenceModel, on_delete=models.CASCADE, null=True, blank=True) 

Loaded with the following data: [{"model": "questions.selfreferencemainmodel", "pk": 1, "fields": {"self_reference": 1}}, {"model": "questions.selfreferencemodel", "pk": 1, "fields": {"parent": null, "text": "Test 1"}}, {"model": "questions.selfreferencemodel", "pk": 2, "fields": {"parent": 1, "text": "Test 2"}}]

And the following API:

class SelfReferenceSchema(ModelSchema):
    parent: 'SelfReferenceSchema' = None
    class Meta:
        model = SelfReferenceModel
        fields = ['id', 'text']
SelfReferenceSchema.update_forward_refs()
class SelfReferenceTestOut(ModelSchema):
    self_reference: SelfReferenceSchema
    class Meta:
        model = SelfReferenceMainModel
        fields = ["id"]
@router.post("/self-reference-test", response={200: SelfReferenceTestOut})
def self_reference_test(request):
    question_stack = SelfReferenceModel.objects.get(pk=1)
    return 200, question_stack

Results in errors in the OpenAPI docs:

Screenshot 2024-06-18 at 7 39 26 PM

And shows an incorrect schema (where parent is shown as a string):

Screenshot 2024-06-18 at 7 39 33 PM

When the endpoint is executed, the following error is returned:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/ninja/operation.py", line 108, in run
    return self._result_to_response(request, result, temporal_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/ninja/operation.py", line 208, in _result_to_response
    validated_object = response_model.model_validate(
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/pydantic/main.py", line 551, in model_validate
    return cls.__pydantic_validator__.validate_python(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for NinjaResponseSchema
response.self_reference
  Field required [type=missing, input_value=<DjangoGetter: <SelfRefer...erenceModel object (1)>>, input_type=DjangoGetter]
    For further information visit https://errors.pydantic.dev/2.7/v/missing

Lastly, here is the generated OpenAPI.json:

{
  "openapi": "3.1.0",
  "info": {
    "title": "NinjaAPI",
    "version": "1.0.0",
    "description": ""
  },
  "paths": {
    "/api/questions/self-reference-test": {
      "post": {
        "operationId": "questions_api_self_reference_test",
        "summary": "Self Reference Test",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SelfReferenceTestOut"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SelfReferenceSchema": {
        "properties": {
          "parent": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SelfReferenceSchema"
              }
            ]
          },
          "id": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "ID"
          },
          "text": {
            "maxLength": 500,
            "title": "Text",
            "type": "string"
          }
        },
        "required": [
          "text"
        ],
        "title": "SelfReferenceSchema",
        "type": "object"
      },
      "SelfReferenceTestOut": {
        "properties": {
          "self_reference": {
            "$ref": "#/components/schemas/SelfReferenceSchema"
          },
          "id": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "ID"
          }
        },
        "required": [
          "self_reference"
        ],
        "title": "SelfReferenceTestOut",
        "type": "object"
      }
    }
  },
  "servers": []
}

Thank you for your assistance in advance!

Versions (please complete the following information):

akkim2 commented 3 months ago

In case anyone is interested, I was able to find a workaround:

+ from typing import Optional, ForwardRef
+ SelfReferenceSchema = ForwardRef('SelfReferenceSchema')
class SelfReferenceSchema(ModelSchema):
-  parent: 'SelfReferenceSchema' = None
+  parent: Optional[SelfReferenceSchema]
    class Meta:
        model = SelfReferenceModel
        fields = ['id', 'text']
- SelfReferenceSchema.update_forward_refs()
class SelfReferenceTestOut(ModelSchema):
    self_reference: SelfReferenceSchema
    class Meta:
        model = SelfReferenceMainModel
        fields = ["id"]
@router.post("/self-reference-test", response={200: SelfReferenceTestOut})
def self_reference_test(request):
    question_stack = SelfReferenceModel.objects.get(pk=1)
    return 200, question_stack