Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Self-Referencing Schemas are Broken #1198

Open
akkim2 opened this issue Jun 19, 2024 · 1 comment
Open

[BUG] Self-Referencing Schemas are Broken #1198

akkim2 opened this issue Jun 19, 2024 · 1 comment

Comments

@akkim2
Copy link

akkim2 commented Jun 19, 2024

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):

  • Python version: 3.12.3
  • Django version: 5.0.6
  • Django-Ninja version: 1.1.0
  • Pydantic version: 2.7.3
@akkim2
Copy link
Author

akkim2 commented Jun 25, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant