Closes #21244: Introduce ability to omit specific fields from REST API responses (#21312)
CI / build (20.x, 3.12) (push) Failing after 14s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 11s
CodeQL / Analyze (actions) (push) Failing after 21s
CodeQL / Analyze (javascript-typescript) (push) Failing after 17s
CodeQL / Analyze (python) (push) Failing after 17s

Introduce support for omitting specific serializer fields via an
`omit` parameter, acting as the inverse of `fields`.
Wire it through the API viewset and queryset optimization helpers
so omitted fields don’t trigger unnecessary annotations/prefetches,
and document the new behavior.
This commit is contained in:
Jeremy Stretch
2026-01-28 16:06:46 -05:00
committed by GitHub
parent 0b507eb207
commit 1526e437f1
4 changed files with 101 additions and 41 deletions
+46 -4
View File
@@ -215,9 +215,51 @@ http://netbox/api/ipam/ip-addresses/ \
If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately.
### Brief Format
### Specifying Fields
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of a prefix looks like this:
A REST API response will include all available fields for the object type by default. If you wish to return only a subset of the available fields, you can append `?fields=` to the URL followed by a comma-separated list of field names. For example, the following request will return only the `id`, `name`, `status`, and `region` fields for each site in the response.
```
GET /api/dcim/sites/?fields=id,name,status,region
```
```json
{
"id": 1,
"name": "DM-NYC",
"status": {
"value": "active",
"label": "Active"
},
"region": {
"id": 43,
"url": "http://netbox:8000/api/dcim/regions/43/",
"display": "New York",
"name": "New York",
"slug": "us-ny",
"description": "",
"site_count": 0,
"_depth": 2
}
}
```
Similarly, you can opt to omit only specific fields by passing the `omit` parameter:
```
GET /api/dcim/sites/?omit=circuit_count,device_count,virtualmachine_count
```
!!! note "The `omit` parameter was introduced in NetBox v4.5.2."
Strategic use of the `fields` and `omit` parameters can drastically improve REST API performance, as the exclusion of fields which reference related objects reduces the number and complexity of underlying database queries needed to generate the response.
!!! note
The `fields` and `omit` parameters should be considered mutually exclusive. If both are passed, `fields` takes precedence.
#### Brief Format
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. It's also more convenient than listing out individual fields via the `fields` or `omit` parameters. As an example, the default (complete) format of a prefix looks like this:
```no-highlight
GET /api/ipam/prefixes/13980/
@@ -270,10 +312,10 @@ GET /api/ipam/prefixes/13980/
}
```
The brief format is much more terse:
The brief format includes only a few fields:
```no-highlight
GET /api/ipam/prefixes/13980/?brief=1
GET /api/ipam/prefixes/13980/?brief=true
```
```json
+19 -15
View File
@@ -1,9 +1,8 @@
from functools import cached_property
from rest_framework import serializers
from rest_framework.utils.serializer_helpers import BindingDict
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from utilities.api import get_related_object_by_attrs
from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
@@ -19,16 +18,18 @@ class BaseModelSerializer(serializers.ModelSerializer):
display_url = NetBoxURLHyperlinkedIdentityField()
display = serializers.SerializerMethodField(read_only=True)
def __init__(self, *args, nested=False, fields=None, **kwargs):
def __init__(self, *args, nested=False, fields=None, omit=None, **kwargs):
"""
Extends the base __init__() method to support dynamic fields.
:param nested: Set to True if this serializer is being employed within a parent serializer
:param fields: An iterable of fields to include when rendering the serialized object, If nested is
True but no fields are specified, Meta.brief_fields will be used.
:param omit: An iterable of fields to omit from the serialized object
"""
self.nested = nested
self._requested_fields = fields
self._include_fields = fields or []
self._omit_fields = omit or []
# Disable validators for nested objects (which already exist)
if self.nested:
@@ -36,8 +37,8 @@ class BaseModelSerializer(serializers.ModelSerializer):
# If this serializer is nested but no fields have been specified,
# default to using Meta.brief_fields (if set)
if self.nested and not fields:
self._requested_fields = getattr(self.Meta, 'brief_fields', None)
if self.nested and not fields and not omit:
self._include_fields = getattr(self.Meta, 'brief_fields', None)
super().__init__(*args, **kwargs)
@@ -54,16 +55,19 @@ class BaseModelSerializer(serializers.ModelSerializer):
@cached_property
def fields(self):
"""
Override the fields property to check for requested fields. If defined,
return only the applicable fields.
Override the fields property to return only specifically requested fields if needed.
"""
if not self._requested_fields:
return super().fields
fields = super().fields
# Include only requested fields
if self._include_fields:
for field_name in set(fields) - set(self._include_fields):
fields.pop(field_name, None)
# Remove omitted fields
for field_name in set(self._omit_fields):
fields.pop(field_name, None)
fields = BindingDict(self)
for key, value in self.get_fields().items():
if key in self._requested_fields:
fields[key] = value
return fields
@extend_schema_field(OpenApiTypes.STR)
+18 -13
View File
@@ -5,13 +5,13 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import router, transaction
from django.db.models import ProtectedError, RestrictedError
from django_pglocks import advisory_lock
from netbox.constants import ADVISORY_LOCK_KEYS
from rest_framework import mixins as drf_mixins
from rest_framework import status
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from netbox.api.serializers.features import ChangeLogMessageSerializer
from netbox.constants import ADVISORY_LOCK_KEYS
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
from utilities.exceptions import AbortRequest
from utilities.query import reapply_model_ordering
@@ -59,33 +59,38 @@ class BaseViewSet(GenericViewSet):
serializer_class = self.get_serializer_class()
# Dynamically resolve prefetches for included serializer fields and attach them to the queryset
if prefetch := get_prefetches_for_serializer(serializer_class, fields_to_include=self.requested_fields):
if prefetch := get_prefetches_for_serializer(serializer_class, **self.field_kwargs):
qs = qs.prefetch_related(*prefetch)
# Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset
if annotations := get_annotations_for_serializer(serializer_class, fields_to_include=self.requested_fields):
if annotations := get_annotations_for_serializer(serializer_class, **self.field_kwargs):
qs = qs.annotate(**annotations)
return qs
def get_serializer(self, *args, **kwargs):
# If specific fields have been requested, pass them to the serializer
if self.requested_fields:
kwargs['fields'] = self.requested_fields
# Pass the fields/omit kwargs (if specified by the request) to the serializer
kwargs.update(**self.field_kwargs)
return super().get_serializer(*args, **kwargs)
@cached_property
def requested_fields(self):
def field_kwargs(self):
"""Return a dictionary of keyword arguments to be passed when instantiating the serializer."""
# An explicit list of fields was requested
if requested_fields := self.request.query_params.get('fields'):
return requested_fields.split(',')
return {'fields': requested_fields.split(',')}
# An explicit list of fields to omit was requested
if omit_fields := self.request.query_params.get('omit'):
return {'omit': omit_fields.split(',')}
# Brief mode has been enabled for this request
elif self.brief:
if self.brief:
serializer_class = self.get_serializer_class()
return getattr(serializer_class.Meta, 'brief_fields', None)
return None
if brief_fields := getattr(serializer_class.Meta, 'brief_fields', None):
return {'fields': brief_fields}
return {}
class NetBoxReadOnlyModelViewSet(
+18 -9
View File
@@ -93,18 +93,23 @@ def get_view_name(view):
return drf_get_view_name(view)
def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
def get_prefetches_for_serializer(serializer_class, fields=None, omit=None):
"""
Compile and return a list of fields which should be prefetched on the queryset for a serializer.
"""
if fields is not None and omit is not None:
raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
model = serializer_class.Meta.model
# If fields are not specified, default to all
if not fields_to_include:
fields_to_include = serializer_class.Meta.fields
fields_to_include = fields or serializer_class.Meta.fields
fields_to_omit = omit or []
prefetch_fields = []
for field_name in fields_to_include:
if field_name in fields_to_omit:
continue
serializer_field = serializer_class._declared_fields.get(field_name)
# Determine the name of the model field referenced by the serializer field
@@ -132,19 +137,23 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
return prefetch_fields
def get_annotations_for_serializer(serializer_class, fields_to_include=None):
def get_annotations_for_serializer(serializer_class, fields=None, omit=None):
"""
Return a mapping of field names to annotations to be applied to the queryset for a serializer.
"""
annotations = {}
# If specific fields are not specified, default to all
if not fields_to_include:
fields_to_include = serializer_class.Meta.fields
if fields is not None and omit is not None:
raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
model = serializer_class.Meta.model
# If fields are not specified, default to all
fields_to_include = fields or serializer_class.Meta.fields
fields_to_omit = omit or []
annotations = {}
for field_name, field in serializer_class._declared_fields.items():
if field_name in fields_to_omit:
continue
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
related_field = getattr(model, field.relation).field
annotations[field_name] = count_related(related_field.model, related_field.name)