diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py index 57dc842b7..939868e40 100644 --- a/netbox/core/api/schema.py +++ b/netbox/core/api/schema.py @@ -215,8 +215,10 @@ class NetBoxAutoSchema(AutoSchema): def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str: name = super()._get_serializer_name(serializer, direction, bypass_extensions) - # If this serializer is nested, prepend its name with "Brief" - if getattr(serializer, 'nested', False): + # If this serializer is nested, prepend its name with "Brief". Serializers which declare an explicit + # Meta.ref_name are exempt: those are brief by design and have no complete form in the schema, so the + # prefix would only rename an existing component to no purpose. See #22989. + if getattr(serializer, 'nested', False) and not getattr(getattr(serializer, 'Meta', None), 'ref_name', None): name = f'Brief{name}' return name diff --git a/netbox/core/tests/test_openapi_schema.py b/netbox/core/tests/test_openapi_schema.py index 00da4ae3c..50ac6c970 100644 --- a/netbox/core/tests/test_openapi_schema.py +++ b/netbox/core/tests/test_openapi_schema.py @@ -131,7 +131,7 @@ class OpenAPISchemaTestCase(TestCase): for component, field, ref in ( ('Site', 'asns', 'BriefASN'), ('ConfigContext', 'sites', 'BriefSite'), - ('ASN', 'sites', 'BriefASNSite'), + ('Interface', 'tagged_vlans', 'BriefVLAN'), ): with self.subTest(component=component, field=field): self.assertEqual( @@ -145,6 +145,28 @@ class OpenAPISchemaTestCase(TestCase): {'id', 'url', 'display', 'asn', 'description'} ) + def test_ref_name_exempts_serializer_from_brief_prefix(self): + """ + A serializer which declares an explicit Meta.ref_name keeps that name when nested, rather than + acquiring a Brief prefix. These serializers are brief by design and have no complete form in the + schema, so prefixing them would rename an existing component to no purpose. + + Refs: #22989 + """ + components = self.schema['components']['schemas'] + + for component, field, ref in ( + ('ASN', 'sites', 'ASNSite'), + ('ObjectPermission', 'groups', 'NestedGroup'), + ('ObjectPermission', 'users', 'NestedUser'), + ): + with self.subTest(component=component, field=field): + self.assertEqual( + components[component]['properties'][field]['items']['$ref'], + f'#/components/schemas/{ref}' + ) + self.assertNotIn(f'Brief{ref}', components) + def test_non_nested_related_fields_reference_full_components(self): """ A SerializedPKRelatedField declared without nested=True must continue to reference the diff --git a/netbox/ipam/api/serializers_/asns.py b/netbox/ipam/api/serializers_/asns.py index c5c95bf19..fe7153cf3 100644 --- a/netbox/ipam/api/serializers_/asns.py +++ b/netbox/ipam/api/serializers_/asns.py @@ -54,6 +54,7 @@ class ASNSiteSerializer(PrimaryModelSerializer): model = Site fields = ('id', 'url', 'display', 'name', 'description', 'slug') brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') + ref_name = 'ASNSite' class ASNSerializer(PrimaryModelSerializer): diff --git a/netbox/users/api/serializers_/nested.py b/netbox/users/api/serializers_/nested.py index b268776b5..a3e9ffaa2 100644 --- a/netbox/users/api/serializers_/nested.py +++ b/netbox/users/api/serializers_/nested.py @@ -15,6 +15,7 @@ class NestedGroupSerializer(WritableNestedSerializer): class Meta: model = models.Group fields = ['id', 'url', 'display_url', 'display', 'name'] + ref_name = 'NestedGroup' class NestedUserSerializer(WritableNestedSerializer): @@ -22,6 +23,7 @@ class NestedUserSerializer(WritableNestedSerializer): class Meta: model = models.User fields = ['id', 'url', 'display_url', 'display', 'username'] + ref_name = 'NestedUser' @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj):