Bishwas-py / djapy

No bullshit, Django Rest API Framework
https://djapy.io
59 stars 3 forks source link

Nested Schema Support #21

Closed codernirdesh closed 3 months ago

codernirdesh commented 3 months ago

I am new to Python, and I'm facing the following nested Schema issue:. I don't know if this is my issue or Djapy's parser.

Schema

class Gender(str, Enum):
    male = "male"
    female = "female"

class PersonSchema(Schema):
    id: int
    name: str
    gender: Gender
    place_of_birth: Optional[str]
    date_of_birth: date

class FamilySchema(Schema):
    id: int
    name: str
    level: int
    members: Optional[PersonSchema]

Model

class Family(models.Model):
    class Meta:
        ordering = ["name"]
        verbose_name_plural = "Families"

    name = models.CharField(max_length=100)
    members = models.ManyToManyField(Person, related_name="families")
    level = models.PositiveIntegerField()

    deleted = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return f"{self.name} - Level {self.level}"

View code

@djapify(allowed_method="GET", tags=["Family Tree"])
def get_all_families(request) -> {200: List[FamilySchema], 404: MessageOut, 500: MessageOut}:  # type: ignore
    """
    Retrieve all families that are not deleted.
    """
    try:
        families: List[FamilySchema] = Family.objects.all()
        # Print first family details
        logIt(
            "๐Ÿš€ family_tree/views.py ~ get_all_families ~ families[0]",
            families[0]
        )
        logIt(
            "๐Ÿš€ family_tree/views.py ~ get_all_families ~ families.count",
            families.count(),
        )
        if families.count() > 0:
            return 200, families
        else:
            logIt(
                "๐Ÿš€ family_tree/views.py ~ get_all_families ~ No families found",
                " No families found",
            )
            return 404, MessageOut(
                "No families found",
                "no_families_found",
                "error",
            )
    except Exception as e:
        return 500, MessageOut(
            str(e),
            "database_error",
            "error",
        )

Output on hitting the endpoint

๐Ÿš€ family_tree/views.py ~ get_all_families ~ families[0] ~ Test - 55 - Level 55
๐Ÿš€ family_tree/views.py ~ get_all_families ~ families.count ~ 2
ERROR:root:Unable to serialize unknown type: <class 'django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager'>
Traceback (most recent call last):
  File "/run/media/nirdesh/Nirdesh/python/blog-django/benv/lib64/python3.12/site-packages/djapy/core/dec.py", line 242, in _wrapped_view
    parsed_data = parser.parse_response_data()
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/nirdesh/Nirdesh/python/blog-django/benv/lib64/python3.12/site-packages/djapy/core/parser.py", line 105, in parse_response_data
    destructured_object_data = validated_obj.model_dump(mode="json", by_alias=True)
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/nirdesh/Nirdesh/python/blog-django/benv/lib64/python3.12/site-packages/pydantic/main.py", line 314, in model_dump
    return self.__pydantic_serializer__.to_python(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.PydanticSerializationError: Unable to serialize unknown type: <class 'django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager'>
Internal Server Error: /family/families
ERROR:django.request:Internal Server Error: /family/families
Bishwas-py commented 3 months ago

Hey @codernirdesh , It seems like the List is the wrapper for members; Try this:

class FamilySchema(Schema):
    id: int
    name: str
    level: int
    members: PersonSchema

    @field_validator('members', mode='before')
    def assign_members(cls, members):
        return members.all()
codernirdesh commented 3 months ago

I'm still getting error. Response :

{
  "error": [
    {
      "type": "missing",
      "loc": [
        "response",
        0,
        "members",
        "id"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Mohipati>, <Person: Narapati>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        0,
        "members",
        "name"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Mohipati>, <Person: Narapati>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        0,
        "members",
        "gender"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Mohipati>, <Person: Narapati>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        0,
        "members",
        "place_of_birth"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Mohipati>, <Person: Narapati>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        0,
        "members",
        "date_of_birth"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Mohipati>, <Person: Narapati>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        1,
        "members",
        "id"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Narapati>, <Person: Nirdesh Pokharel>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        1,
        "members",
        "name"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Narapati>, <Person: Nirdesh Pokharel>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        1,
        "members",
        "gender"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Narapati>, <Person: Nirdesh Pokharel>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        1,
        "members",
        "place_of_birth"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Narapati>, <Person: Nirdesh Pokharel>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    },
    {
      "type": "missing",
      "loc": [
        "response",
        1,
        "members",
        "date_of_birth"
      ],
      "msg": "Field required",
      "input": "<QuerySet [<Person: Narapati>, <Person: Nirdesh Pokharel>]>",
      "url": "https://errors.pydantic.dev/2.6/v/missing"
    }
  ],
  "error_count": 10,
  "title": "output"
}

Console:

Families :  2
ERROR:root:10 validation errors for output
response.0.members.id
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.0.members.name
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.0.members.gender
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.0.members.place_of_birth
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.0.members.date_of_birth
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.id
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.name
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.gender
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.place_of_birth
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.date_of_birth
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
Traceback (most recent call last):
  File "/run/media/nirdesh/Nirdesh/python/blog-django/benv/lib64/python3.12/site-packages/djapy/core/dec.py", line 242, in _wrapped_view
    parsed_data = parser.parse_response_data()
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/nirdesh/Nirdesh/python/blog-django/benv/lib64/python3.12/site-packages/djapy/core/parser.py", line 99, in parse_response_data
    validated_obj = response_model.model_validate(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/run/media/nirdesh/Nirdesh/python/blog-django/benv/lib64/python3.12/site-packages/pydantic/main.py", line 509, in model_validate
    return cls.__pydantic_validator__.validate_python(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 10 validation errors for output
response.0.members.id
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.0.members.name
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.0.members.gender
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.0.members.place_of_birth
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.0.members.date_of_birth
  Field required [type=missing, input_value=<QuerySet [<Person: Mohip...i>, <Person: Narapati>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.id
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.name
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.gender
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.place_of_birth
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
response.1.members.date_of_birth
  Field required [type=missing, input_value=<QuerySet [<Person: Narap...son: Nirdesh Pokharel>]>, input_type=QuerySet]
    For further information visit https://errors.pydantic.dev/2.6/v/missing
Bad Request: /family/families/
WARNING:django.request:Bad Request: /family/families/
[08/Apr/2024 06:08:25] "GET /family/families/ HTTP/1.1" 400 2177
Bishwas-py commented 3 months ago

Can you share your Person model?

codernirdesh commented 3 months ago

Sure. Models

# family_tree/models.py
from django.db import models

class Person(models.Model):
    class Meta:
        ordering = ["name"]
        verbose_name_plural = "People"

    GENDER_CHOICES = [
        ("male", "Male"),
        ("female", "Female"),
    ]

    name = models.CharField(max_length=100)
    gender = models.CharField(max_length=10, choices=GENDER_CHOICES)
    date_of_birth = models.DateField(null=True, blank=True)
    place_of_birth = models.CharField(max_length=100, null=True, blank=True)

    deleted = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return self.name

class Family(models.Model):
    class Meta:
        ordering = ["name"]
        verbose_name_plural = "Families"

    name = models.CharField(max_length=100)
    members = models.ManyToManyField(Person, related_name="families")
    level = models.PositiveIntegerField()

    deleted = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return f"{self.name} - Level {self.level}"

class Relationship(models.Model):
    class Meta:
        ordering = ["person"]
        verbose_name_plural = "Relationships"

    RELATIONSHIP_CHOICES = [
        ("parent", "Parent"),
        ("child", "Child"),
        ("spouse", "Spouse"),
    ]

    person = models.ForeignKey(
        Person, related_name="relationships", on_delete=models.CASCADE
    )
    related_person = models.ForeignKey(Person, on_delete=models.CASCADE)
    relationship_type = models.CharField(max_length=10, choices=RELATIONSHIP_CHOICES)

    deleted = models.DateTimeField(null=True, blank=True)

    family = models.ForeignKey(
        Family,
        related_name="relationships",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )

    def __str__(self):
        return f"{self.person} - {self.related_person}: {self.relationship_type}"

Schema

from datetime import date
from enum import Enum
from typing import Optional
from pydantic import field_validator
from typing_extensions import TypedDict
from djapy import Schema

class Gender(str, Enum):
    male = "male"
    female = "female"

class PersonSchema(Schema):
    id: int
    name: str
    gender: Gender
    place_of_birth: Optional[str]
    date_of_birth: date

class FamilySchema(Schema):
    id: int
    name: str
    level: int
    members: PersonSchema

    @field_validator("members", mode="before")
    def assign_members(cls, members):
        return members.all()

class MessageOut(TypedDict):
    message: str
    message_type: str
    alias: str
Bishwas-py commented 3 months ago

Sorry man, I also did a mistake on that previous schema:

class FamilySchema(Schema):
    id: int
    name: str
    level: int
    members: List[PersonSchema]

    @field_validator('members', mode='before')
    def assign_members(cls, members):
        return members.all()
codernirdesh commented 3 months ago
class FamilySchema(Schema):
    id: int
    name: str
    level: int
    members: List[PersonSchema]

    @field_validator('members', mode='before')
    def assign_members(cls, members):
        return members.all()

It worked! Thank you for helping me out.