tfranzel / drf-spectacular

Sane and flexible OpenAPI 3 schema generation for Django REST framework.
https://drf-spectacular.readthedocs.io
BSD 3-Clause "New" or "Revised" License
2.42k stars 266 forks source link

Can't extract type from GeneratedField #1166

Open ckarli opened 10 months ago

ckarli commented 10 months ago

Describe the bug Types can't be extracted from newly added GeneratedField. Field must be added explicitly to the serializer with the correct type as defined in the "output_field". Otherwise it gives the error below.

backend-1 | Traceback (most recent call last): backend-1 | File "/usr/local/lib/python3.12/site-packages/django/contrib/staticfiles/handlers.py", line 80, in call backend-1 | return self.application(environ, start_response) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/django/init.py", line 165, in sentry_patched_wsgi_handler backend-1 | return SentryWsgiMiddleware(bound_old_app, use_x_forwarded_for)( backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/wsgi.py", line 115, in call backend-1 | reraise(_capture_exception(hub)) backend-1 | File "/usr/local/lib/python3.12/site-packages/sentry_sdk/_compat.py", line 127, in reraise backend-1 | raise value backend-1 | File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/wsgi.py", line 108, in call backend-1 | rv = self.app( backend-1 | ^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django/core/handlers/wsgi.py", line 124, in call backend-1 | response = self.get_response(request) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/django/init.py", line 460, in sentry_patched_get_response backend-1 | rv = old_get_response(self, request) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django/core/handlers/base.py", line 140, in get_response backend-1 | response = self._middleware_chain(request) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django/core/handlers/exception.py", line 57, in inner backend-1 | response = response_for_exception(request, exc) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django/core/handlers/exception.py", line 140, in response_for_exception backend-1 | response = handle_uncaught_exception( backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django/core/handlers/exception.py", line 181, in handle_uncaught_exception backend-1 | return debug.technical_500_response(request, exc_info) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django_extensions/management/technical_response.py", line 40, in null_technical_500_response backend-1 | raise exc_value.with_traceback(tb) backend-1 | File "/usr/local/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner backend-1 | response = get_response(request) backend-1 | ^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response backend-1 | response = wrapped_callback(request, *callback_args, callback_kwargs) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/django/views.py", line 84, in sentry_wrapped_callback backend-1 | return callback(request, *args, *kwargs) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django/views/decorators/csrf.py", line 65, in _view_wrapper backend-1 | return view_func(request, args, kwargs) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/django/views/generic/base.py", line 104, in view backend-1 | return self.dispatch(request, *args, *kwargs) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/rest_framework/views.py", line 509, in dispatch backend-1 | response = self.handle_exception(exc) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/rest_framework/views.py", line 469, in handle_exception backend-1 | self.raise_uncaught_exception(exc) backend-1 | File "/usr/local/lib/python3.12/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception backend-1 | raise exc backend-1 | File "/usr/local/lib/python3.12/site-packages/rest_framework/views.py", line 506, in dispatch backend-1 | response = handler(request, args, **kwargs) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/views.py", line 84, in get backend-1 | return self._get_schema_response(request) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/views.py", line 92, in _get_schema_response backend-1 | data=generator.get_schema(request=request, public=self.serve_public), backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/generators.py", line 281, in get_schema backend-1 | paths=self.parse(request, public), backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/generators.py", line 252, in parse backend-1 | operation = view.schema.get_operation( backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 99, in get_operation backend-1 | request_body = self._get_request_body() backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 1295, in _get_request_body backend-1 | schema, partial_request_body_required = self._get_request_for_media_type(serializer, direction) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 1327, in _get_request_for_media_type backend-1 | component = self.resolve_serializer(serializer, direction) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 1605, in resolve_serializer backend-1 | component.schema = self._map_serializer(serializer, direction, bypass_extensions) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 927, in _map_serializer backend-1 | schema = self._map_basic_serializer(serializer, direction) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 1026, in _map_basic_serializer backend-1 | schema = self._map_serializer_field(field, direction) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 904, in _map_serializer_field backend-1 | schema = self._map_model_field(field.model_field, direction) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | File "/usr/local/lib/python3.12/site-packages/drf_spectacular/openapi.py", line 612, in _map_model_field backend-1 | return self._map_serializer_field(field_cls(), direction) backend-1 | ^^^^^^^^^^^^^^^^^^^^^^^ backend-1 | TypeError: DecimalField.init() missing 2 required positional arguments: 'max_digits' and 'decimal_places'

To Reproduce

class Invoice(models.Model):
    #...omitting amount_from and xrate_from fields
    amount= models.GeneratedField(
        expression=F("amount_from") * F("xrate_from"),
        output_field=models.DecimalField(max_digits=19, decimal_places=4),
        db_persist=True,
    )

Use GeneratedField in a model, and use this model in a ModelSerializer's fields parameter.

class InvoiceSerializer(serializers.ModelSerializer):

class Meta:
    model = Invoice
    fields = ("amount",)

Expected behavior Field type must be inferred from output_field

tfranzel commented 10 months ago

Interesting. I was not aware of that new field type. Will have to investigate on how to handle the field. Seems like output_field is pretty much the only option for extraction.

roniemartinez commented 4 months ago

We are getting the same error using GeneratedField.

TypeError: DecimalField.__init__() missing 2 required positional arguments: 'max_digits' and 'decimal_places'
aan1173 commented 1 day ago

I meet the similar error using 'GeneratedField' with 'DecimalField' TypeError: DecimalField.__init__() missing 2 required positional arguments: 'max_digits' and 'decimal_places'

class Product(models.Model):
    name = models.CharField(max_length=255)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    number = models.IntegerField()
    description = models.TextField()
    release_date = models.DateField()
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
    ERROR_in_swaggerUI = models.GeneratedField(
        expression=F("price") * F("number"),
        output_field=models.DecimalField(max_digits=15, decimal_places=2),
        db_persist=True,
    )
    OK_in_swaggerUI = models.GeneratedField(
        expression=F("price") * F("number"),
        output_field=models.FloatField(),
        db_persist=True,
    )

Everything is ok if we comment out the "ERROR_in_swaggerUI" above

tfranzel commented 1 day ago

Sry this kinda fell of the waggon. I did indeed tried to parse this field for some time, but I turned out to be weirdly complicated.

I will make another attempt at this, but if it does not work out, at least we should fall back to a str default and not raise any exceptions until we can figure out a better solution.