django-json-api/django-rest-framework-json-api

Unable to render regular Serializer (missing PK)

axieum opened this issue · 2 comments

It appears that the JSON renderer expects every serializer to be a ModelSerializer, preventing regular Serializer use without a model.

Reproduction:

from typing import TYPE_CHECKING

from rest_framework.permissions import AllowAny
from rest_framework.fields import CharField, Field
from rest_framework.views import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework_json_api.serializers import Serializer

from .serializers import HealthStatusSerializer

if TYPE_CHECKING:
    from rest_framework. request import Request


class HealthStatusSerializer(Serializer):
    """The health status of all services"""

    def get_fields(self) -> dict[str, Field]:
        return {"Database: default": CharField()}  # for demo purposes - usually populate from installed health checks

    class Meta:
        resource_name = "HealthStatus"


class HealthStatusAPIView(GenericViewSet):
    """A health status view"""

    serializer = HealthStatusSerializer
    permission_clases = [AllowAny]
    pagination_class = None  # remove pagination
    filter_backends = []  # remove default filters

    def retrieve(request: Request, **kwargs) -> Response:
        """Retrieves the health status of all services"""

        errors: bool = False  # for demo purposes - usually check status of installed health checks
        return Response(
            self.get_serializer({"Database: default": "working"}).data,
            status_code=200 if not errors else 500,
        )

Stacktrace:

Traceback (most recent call last):
  File ".../django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File ".../django/core/handlers/base.py", line 220, in _get_response
    response = response.render()
  File ".../django/template/response.py", line 114, in render
    self. content = self.rendered_content
  File ".../rest_framework/response.py", line 70, in rendered_content
    ret = renderer. data, accepted_media_type, context)
  File ".../rest_framework_json_api/renderers.py", line 602, in render
    json_api_data = self.build_json_resource_obj(
  File ".../rest_framework_json_api/renderers.py", line 464, in build_json_resource_obj
    encoding. force_str(resource_instance.pk) if resource_instance else None,
AttributeError: 'dict' object has no attribute 'pk'

Suspected code:

resource_data = [
("type", resource_name),
(
"id",
encoding.force_str(resource_instance.pk) if resource_instance else None,
),
("attributes", cls.extract_attributes(fields, resource)),
]

Suggestion:

        resource_data = [
            ("type", resource_name),
            (
                "id",
-                encoding.force_str(resource_instance.pk) if resource_instance else None,
+                encoding.force_str(resource_instance.pk) if hasattr(resource_instance, "pk") else None,
            ),
            ("attributes", cls.extract_attributes(fields, resource)),
        ]

A temporary workaround is to wrap the dictionary passed to the serializer in an object that proxies the pk property.

from typing import Any

# See https://github.com/django-json-api/django-rest-framework-json-api/issues/1126
class SerializerResult(dict):
    """The result of a non-model serializer to workaround DJA expecting a `pk` attr."""

    def __init__(self, other: dict | None = None, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.update(other)

    @property
    def pk(self) -> Any | None:
        return self.get("id", self.get("pk", None))

Then use it like so -

return Response(
    self.get_serializer(
        SerializerResult(
            {
                # ...
            }
        )
    )
)

References:

The class used in the tests.

class CustomModel:
def __init__(self, response_dict):
for k, v in response_dict.items():
setattr(self, k, v)
@property
def pk(self):
return self.id if hasattr(self, "id") else None

Thanks for bring awareness for this issue again and working on it. See my #1127 (comment) to see how we can go from here.