Просмотр исходного кода

Fixes #22154: Correct OpenAPI schema regarding relation counts for nested objects

Arthur Hanson 5 дней назад
Родитель
Сommit
7a1dfb58e3
2 измененных файлов с 35 добавлено и 1 удалено
  1. 10 0
      netbox/netbox/api/serializers/base.py
  2. 25 1
      netbox/netbox/tests/test_api.py

+ 10 - 0
netbox/netbox/api/serializers/base.py

@@ -4,6 +4,7 @@ from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 
+from netbox.api.fields import RelatedObjectCountField
 from utilities.api import get_related_object_by_attrs
 
 from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
@@ -69,6 +70,15 @@ class BaseModelSerializer(serializers.ModelSerializer):
         for field_name in set(self._omit_fields):
             fields.pop(field_name, None)
 
+        # Related object counts are populated by annotations applied to the viewset's queryset, but these
+        # annotations are not applied when the object is represented as a nested (brief) related object. Omit
+        # these fields when serializing a nested object to avoid advertising fields that will never be populated
+        # (and which would otherwise be declared as required in the generated OpenAPI schema). See #22154.
+        if self.nested:
+            for field_name, field in list(fields.items()):
+                if isinstance(field, RelatedObjectCountField):
+                    fields.pop(field_name)
+
         return fields
 
     @extend_schema_field(OpenApiTypes.STR)

+ 25 - 1
netbox/netbox/tests/test_api.py

@@ -6,8 +6,9 @@ from django.urls import reverse
 from rest_framework.exceptions import ValidationError
 from rest_framework.request import Request
 
+from dcim.api.serializers import RackSerializer
 from netbox.api.exceptions import QuerySetNotOrdered
-from netbox.api.fields import IntegerRangeSerializer
+from netbox.api.fields import IntegerRangeSerializer, RelatedObjectCountField
 from netbox.api.pagination import NetBoxPagination
 from users.models import Token
 from utilities.testing import APITestCase
@@ -48,6 +49,29 @@ class AppTestCase(APITestCase):
         self.assertEqual(response.data['id'], self.user.pk)
 
 
+class RelatedObjectCountFieldTestCase(TestCase):
+    """
+    RelatedObjectCountFields are populated by annotations applied to a viewset's queryset, which are only
+    added when serializing an object via its own endpoint (including ?brief=1). They are never annotated when
+    the object is rendered as a nested related object, so they must be omitted from nested representations to
+    keep the generated OpenAPI schema honest. See #22154.
+    """
+    def test_count_field_omitted_when_nested(self):
+        """A nested serializer must drop RelatedObjectCountFields (e.g. RackSerializer.device_count)."""
+        serializer = RackSerializer(nested=True)
+        count_fields = [
+            name for name, field in serializer.fields.items() if isinstance(field, RelatedObjectCountField)
+        ]
+        self.assertEqual(count_fields, [])
+        self.assertNotIn('device_count', serializer.fields)
+
+    def test_count_field_retained_in_brief_mode(self):
+        """?brief=1 (fields=brief_fields, not nested) must retain RelatedObjectCountFields."""
+        serializer = RackSerializer(fields=RackSerializer.Meta.brief_fields)
+        self.assertIn('device_count', serializer.fields)
+        self.assertIsInstance(serializer.fields['device_count'], RelatedObjectCountField)
+
+
 class OptionalLimitOffsetPaginationTestCase(TestCase):
 
     def setUp(self):