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.38k stars 264 forks source link

Nullable source of ReadOnlyField #1306

Open jennydaman opened 3 weeks ago

jennydaman commented 3 weeks ago

Describe the bug

I added a test for this bug to a fork. See https://github.com/jennydaman/drf-spectacular/commit/c124c7a10bf9662bad5403f69ca00914b2872db9 To Reproduce

import uuid
from django.db import models
from rest_framework import serializers, viewsets
from tests import assert_schema, generate_schema
class Album(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Song(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    album = models.ForeignKey(Album, on_delete=models.CASCADE, null=True)
class SongSerializer(serializers.ModelSerializer):
    album_id = serializers.ReadOnlyField(source='album.id')
    class Meta:
        fields = ['id', 'album_id']
        model = Song
class SongModelViewset(viewsets.ModelViewSet):
    serializer_class = SongSerializer
    queryset = Song.objects.none()

Expected behavior

The album_id field should be nullable:

components:
  schemas:
    Song:
      type: object
      properties:
        album_id:
          type: string
          format: uuid
          readOnly: true
          nullable: true  # <-- expected to be in schema, actually missing
jennydaman commented 3 weeks ago

One down, 98 to go... similar bug occurs with HyperlinkedRelatedField

edit: to handle a case like

import uuid

from django.db import models
from rest_framework import serializers, viewsets

from tests import assert_schema, generate_schema

class Cubicle(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

class Employee(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    cubicle = models.ForeignKey(Cubicle, on_delete=models.CASCADE, null=True)

class EmployeeSerializer(serializers.HyperlinkedModelSerializer):

    cubicle_url = serializers.HyperlinkedRelatedField(view_name='cubicle-detail', read_only=True)

    class Meta:
        fields = ['id', 'cubicle_url']
        model = Employee

In the example above, EmployeeSerializer.cubicle_url could be null but it is not explicitly created with allow_null=True. In this situation, drf-spectacular could figure this out using some magic involving django.urls.reverse and django.urls.resolve. Unfortunately I myself don't have the energy to contribute this fix (i.e. https://github.com/tfranzel/drf-spectacular/pull/1307 is ready to merge, it doesn't fix all related bugs but it does add something)