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

This commit is contained in:
Arthur Hanson 2026-06-30 13:05:35 -07:00 committed by GitHub
parent 614d50a564
commit 7a1dfb58e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 35 additions and 1 deletions

View File

@ -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)

View File

@ -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):