From b9f13c6c75b0373220eb891dc220fb87f089d435 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2026 11:20:45 -0400 Subject: [PATCH] Use a consistent structure for field errors --- netbox/dcim/tests/test_api.py | 35 ++++++++++++++++++++++++++++ netbox/netbox/api/viewsets/mixins.py | 13 ++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index b2b75f7a3..2bb4dea6d 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -547,6 +547,41 @@ class SiteTestCase(APIViewTestCases.APIViewTestCase): site.refresh_from_db() self.assertEqual(site.description, '') + def test_bulk_write_objects_null_entry(self): + """ + Address a list endpoint with a list containing a null entry. A null fails before any field + is considered, so its error arrives as a bare list of messages; it must still be reported + as a mapping keyed by field name, as the schema declares. + """ + obj_perm = ObjectPermission(name='Test permission', actions=['change', 'delete']) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) + + site = Site.objects.get(slug='site-1') + initial_count = Site.objects.count() + + for method in ('patch', 'put', 'delete'): + with self.subTest(method=method): + data = [{'id': site.pk, 'description': 'x'}, None] + response = getattr(self.client, method)( + self._get_list_url(), data, format='json', **self.header + ) + + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + self.assertEqual(len(response.data['errors']), 1) + self.assertEqual(response.data['errors'][0]['index'], 1) + + # Reported under the key which every other non-field error uses + entry_errors = response.data['errors'][0]['errors'] + self.assertIsInstance(entry_errors, dict) + self.assertIn('__all__', entry_errors) + + # The valid entry must not have been applied + site.refresh_from_db() + self.assertEqual(site.description, '') + self.assertEqual(Site.objects.count(), initial_count, 'No objects should have been deleted') + def test_bulk_update_objects_duplicate_id_invalid_entry(self): """ PATCH a set of objects in which one object is named twice, once with invalid data and once diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index b84fae4a5..42b49f55f 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -107,6 +107,17 @@ def get_non_list_response(data): return Response({'detail': detail}, status=status.HTTP_400_BAD_REQUEST) +def _as_field_errors(item_errors): + """ + Return the errors reported for one entry of a bulk request as a mapping of field name to + messages. + """ + if isinstance(item_errors, dict): + return item_errors + + return {api_settings.NON_FIELD_ERRORS_KEY: item_errors} + + def get_invalid_entries_response(entry_errors): """ Return a structured error Response for the entries of a bulk request which could not be @@ -124,7 +135,7 @@ def get_invalid_entries_response(entry_errors): entry per object in the request (an empty dict where that object was interpretable). """ errors = [ - {'index': i, 'errors': item_errors} + {'index': i, 'errors': _as_field_errors(item_errors)} for i, item_errors in enumerate(entry_errors) if item_errors ]