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

[Feature request] Use typing.Annotated to redefine field types #92

Open
MaksimZayats opened this issue Dec 3, 2023 · 4 comments
Open

Comments

@MaksimZayats
Copy link

Hi, thanks for the amazing project :)

I'm having function like this to create serializers inline:

# python3.12.0
from __future__ import annotations

from typing import Any, cast, get_type_hints

from rest_framework import serializers
from rest_framework_dataclasses.serializers import DataclassSerializer


def as_serializer[DataClass: Any](
    dataclass_type: DataClass,
    **kwargs: Any,
) -> type[DataclassSerializer[DataClass]]:
    """Create a serializer from a dataclass.

    This is a helper function to make it easier to create serializers from dataclasses.
    It is equivalent to:
    ```
    class MySerializer(DataclassSerializer):
        class Meta:
            dataclass = MyDataclass
    ```
    """
    field_overrides: dict[str, serializers.Field] = {}

    type_hints = get_type_hints(dataclass_type, include_extras=True)
    for field_name, field_type in type_hints.items():
        metadata = getattr(field_type, "__metadata__", None)
        if not metadata:
            continue

        for value in metadata:
            if not isinstance(value, serializers.Field):
                continue

            field_overrides[field_name] = value
            break

    return cast(
        type[DataclassSerializer[DataClass]],
        type(
            f"{dataclass_type.__name__}Serializer",
            (DataclassSerializer,),
            {
                "Meta": type("Meta", (), {"dataclass": dataclass_type}),
                **field_overrides,
                **kwargs,
            },
        ),
    )

So now I can create serializers from dataclasses that looks like this:

@dataclass
class UploadDocumentRequest:
    file: Annotated[
        InMemoryUploadedFile,
        serializers.FileField(),
    ]


serializer_class = as_serializer(UploadDocumentRequest)

Without checking for Annotated types, I got this error:

NotImplementedError: Automatic serializer field deduction not supported for field 'file' on 'UploadDocumentRequest' of type '<class 'django.core.files.uploadedfile.InMemoryUploadedFile'>' (during search for field of type '<class 'django.core.files.uploadedfile.InMemoryUploadedFile'>').

I was wondering if it's possible to implement discovery of fields from Annotated[...] on your side.

@oxan
Copy link
Owner

oxan commented Dec 3, 2023

Without checking for Annotated types, I got this error:

I don't think this has anything to do with Annotated types, but instead with InMemoryUploadedFile not being a recognized type. Something like Annotated[int] should work fine already.

I was wondering if it's possible to implement discovery of fields from Annotated[...] on your side.

It's certainly possible, but I think the more important question is whether it's desirable. There's already support for specifying the serializer field type in the dataclass definition like this:

@dataclass
class UploadDocumentRequest:
    file: InMemoryUploadedFile = dataclasses.field(metadata={'serializer_field': fields.FileField()})

I suppose it's a bit more verbose, but it's also more generic: you can also add a serializer_kwargs key in the metadata to specify any arguments for the serializer field, which is harder to do with type annotations. If the verbosity bothers you, it's also possible to define a simple helper function to abbreviate it.

@MaksimZayats
Copy link
Author

you can also add a serializer_kwargs key in the metadata to specify any arguments for the serializer field, which is harder to do with type annotations.

It is also possible with Annotated because it's not an annotation, but an instance of the field itself:

@dataclasses.dataclass
class Person:
    # email: str = dataclasses.field(metadata={'serializer_field': fields.EmailField()})
    email: Annotated[str, fields.EmailField()]
 
    # age: int = dataclasses.field(metadata={'serializer_kwargs': {'min_value': 0}})
    age: Annotated[int, fields.IntegerField(min_value=0)]

@oxan
Copy link
Owner

oxan commented Dec 25, 2023

It is also possible with Annotated because it's not an annotation, but an instance of the field itself:

Ah yeah, that's true, though it's a bit more limited, as it requires you to specify all the required arguments of the field, instead of just the ones you want to override. My point remains standing though, we already have a way to override fields within the dataclass declaration, and I don't see a good reason to add another one (yet).

@MaksimZayats
Copy link
Author

MaksimZayats commented Dec 29, 2023

I don't see a good reason to add another one (yet).

In my case, I have a common type that used between couple serializers:

DocumentsType: TypeAlias = Annotated[
    list[models.Document],
    PrimaryKeyRelatedField(
        queryset=models.Document.objects.filter(deleted_at=None),
        many=True,
    ),
]

@dataclass
class A:
    documents: DocumentsType

@dataclass
class B:
    documents: DocumentsType
    

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

2 participants