Merge pull request #22275 from jniec-js/main

Closes: #22245: Fix OpenAPI request schemas for bulk update endpoints
This commit is contained in:
bctiemann 2026-06-02 21:19:42 -04:00 committed by GitHub
commit c1d69ebae6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 102 additions and 3 deletions

View File

@ -138,14 +138,30 @@ class NetBoxAutoSchema(AutoSchema):
return super().get_operation_id()
def get_request_serializer(self) -> typing.Any:
# bulk operations should specify a list
serializer = super().get_request_serializer()
# Bulk update/partial-update has a special request shape: a list of
# writable objects plus a required `id` field. The normal writable
# serializer omits `id` because it is read-only, so don't use the generic
# bulk handling for these actions.
action = getattr(self.view, 'action', None)
if action in ('bulk_update', 'bulk_partial_update'):
get_bulk_update_request_serializer = getattr(
self.view,
'get_bulk_update_request_serializer',
None,
)
if get_bulk_update_request_serializer is not None:
return get_bulk_update_request_serializer(
partial=(action == 'bulk_partial_update' or self.method == 'PATCH')
)
# Bulk creates/deletes should specify a list.
if self.is_bulk_action:
return type(serializer)(many=True)
# handle mapping for Writable serializers - adapted from dansheps original code
# for drf-yasg
# handle mapping for Writable serializers - adapted from dansheps original
# code for drf-yasg.
if serializer is not None and self.method in WRITABLE_ACTIONS:
writable_class = self.get_writable_class(serializer)
if writable_class is not None:

View File

@ -1,11 +1,79 @@
import copy
import functools
from rest_framework import serializers
from .features import ChangeLogMessageSerializer
__all__ = (
'BulkOperationSerializer',
'BulkPartialUpdateSchemaMixin',
'BulkUpdateSchemaMixin',
'get_bulk_update_serializer_class'
)
class BulkOperationSerializer(ChangeLogMessageSerializer):
id = serializers.IntegerField()
class BulkUpdateSchemaMixin:
def get_fields(self):
fields = super().get_fields()
# Reuse the runtime bulk-operation ID field so the schema stays in sync
# with the validator that consumes `id` before model serialization.
_id = copy.deepcopy(BulkOperationSerializer().fields['id'])
_id.required = True
fields['id'] = _id
return fields
class BulkPartialUpdateSchemaMixin(BulkUpdateSchemaMixin):
def get_fields(self):
fields = super().get_fields()
for name, field in fields.items():
if name != 'id':
field.required = False
return fields
@functools.cache
def get_bulk_update_serializer_class(serializer_class, *, partial=False):
"""
Return a schema-only serializer for bulk PUT/PATCH requests.
Bulk update requests to a list endpoint require each object to include
the target object's numeric ID, even though `id` is read-only on the
normal model serializer. The runtime code consumes `id` before invoking
the model serializer for each object.
"""
meta = getattr(serializer_class, 'Meta')
if meta.fields == '__all__':
fields = '__all__'
else:
fields = ('id', *[f for f in meta.fields if f != 'id'])
class Meta(meta):
pass
# intentional; this is different than setting fields = fields within class Meta above
Meta.fields = fields
bases = (
(BulkPartialUpdateSchemaMixin, serializer_class)
if partial
else (BulkUpdateSchemaMixin, serializer_class)
)
attrs = {
'Meta': Meta,
'__module__': serializer_class.__module__,
}
prefix = 'PatchedBulk' if partial else 'Bulk'
return type(f'{prefix}{serializer_class.__name__}', bases, attrs)

View File

@ -7,6 +7,7 @@ from rest_framework.response import Response
from core.models import ObjectType
from extras.models import ExportTemplate
from netbox.api.serializers import BulkOperationSerializer
from netbox.api.serializers.bulk import get_bulk_update_serializer_class
__all__ = (
'BulkDestroyModelMixin',
@ -133,6 +134,20 @@ class BulkUpdateModelMixin:
return updated_pks
def get_bulk_update_serializer_class(self, *, partial=False):
return get_bulk_update_serializer_class(
self.get_serializer_class(),
partial=partial,
)
def get_bulk_update_request_serializer(self, *, partial=False):
serializer_class = self.get_bulk_update_serializer_class(partial=partial)
# Important: do NOT pass partial=True here. The partial schema class already
# makes non-id fields optional, and passing partial=True would also make id
# appear optional in OpenAPI.
return serializer_class(many=True)
def bulk_partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.bulk_update(request, *args, **kwargs)