mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-04 06:16:23 -06:00
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
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:
@@ -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.
|
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
|
```no-highlight
|
||||||
GET /api/ipam/prefixes/13980/
|
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
|
```no-highlight
|
||||||
GET /api/ipam/prefixes/13980/?brief=1
|
GET /api/ipam/prefixes/13980/?brief=true
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from functools import cached_property
|
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.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 utilities.api import get_related_object_by_attrs
|
||||||
from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
|
from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
|
||||||
@@ -19,16 +18,18 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
|||||||
display_url = NetBoxURLHyperlinkedIdentityField()
|
display_url = NetBoxURLHyperlinkedIdentityField()
|
||||||
display = serializers.SerializerMethodField(read_only=True)
|
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.
|
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 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
|
: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.
|
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.nested = nested
|
||||||
self._requested_fields = fields
|
self._include_fields = fields or []
|
||||||
|
self._omit_fields = omit or []
|
||||||
|
|
||||||
# Disable validators for nested objects (which already exist)
|
# Disable validators for nested objects (which already exist)
|
||||||
if self.nested:
|
if self.nested:
|
||||||
@@ -36,8 +37,8 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
# If this serializer is nested but no fields have been specified,
|
# If this serializer is nested but no fields have been specified,
|
||||||
# default to using Meta.brief_fields (if set)
|
# default to using Meta.brief_fields (if set)
|
||||||
if self.nested and not fields:
|
if self.nested and not fields and not omit:
|
||||||
self._requested_fields = getattr(self.Meta, 'brief_fields', None)
|
self._include_fields = getattr(self.Meta, 'brief_fields', None)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -54,16 +55,19 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def fields(self):
|
def fields(self):
|
||||||
"""
|
"""
|
||||||
Override the fields property to check for requested fields. If defined,
|
Override the fields property to return only specifically requested fields if needed.
|
||||||
return only the applicable fields.
|
|
||||||
"""
|
"""
|
||||||
if not self._requested_fields:
|
fields = super().fields
|
||||||
return 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
|
return fields
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
|||||||
from django.db import router, transaction
|
from django.db import router, transaction
|
||||||
from django.db.models import ProtectedError, RestrictedError
|
from django.db.models import ProtectedError, RestrictedError
|
||||||
from django_pglocks import advisory_lock
|
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 mixins as drf_mixins
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from netbox.api.serializers.features import ChangeLogMessageSerializer
|
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.api import get_annotations_for_serializer, get_prefetches_for_serializer
|
||||||
from utilities.exceptions import AbortRequest
|
from utilities.exceptions import AbortRequest
|
||||||
from utilities.query import reapply_model_ordering
|
from utilities.query import reapply_model_ordering
|
||||||
@@ -59,33 +59,38 @@ class BaseViewSet(GenericViewSet):
|
|||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
|
|
||||||
# Dynamically resolve prefetches for included serializer fields and attach them to the queryset
|
# 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)
|
qs = qs.prefetch_related(*prefetch)
|
||||||
|
|
||||||
# Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset
|
# 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)
|
qs = qs.annotate(**annotations)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
# Pass the fields/omit kwargs (if specified by the request) to the serializer
|
||||||
# If specific fields have been requested, pass them to the serializer
|
kwargs.update(**self.field_kwargs)
|
||||||
if self.requested_fields:
|
|
||||||
kwargs['fields'] = self.requested_fields
|
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
@cached_property
|
@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
|
# An explicit list of fields was requested
|
||||||
if requested_fields := self.request.query_params.get('fields'):
|
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
|
# Brief mode has been enabled for this request
|
||||||
elif self.brief:
|
if self.brief:
|
||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
return getattr(serializer_class.Meta, 'brief_fields', None)
|
if brief_fields := getattr(serializer_class.Meta, 'brief_fields', None):
|
||||||
return None
|
return {'fields': brief_fields}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class NetBoxReadOnlyModelViewSet(
|
class NetBoxReadOnlyModelViewSet(
|
||||||
|
|||||||
+18
-9
@@ -93,18 +93,23 @@ def get_view_name(view):
|
|||||||
return drf_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.
|
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
|
model = serializer_class.Meta.model
|
||||||
|
|
||||||
# If fields are not specified, default to all
|
# If fields are not specified, default to all
|
||||||
if not fields_to_include:
|
fields_to_include = fields or serializer_class.Meta.fields
|
||||||
fields_to_include = serializer_class.Meta.fields
|
fields_to_omit = omit or []
|
||||||
|
|
||||||
prefetch_fields = []
|
prefetch_fields = []
|
||||||
for field_name in fields_to_include:
|
for field_name in fields_to_include:
|
||||||
|
if field_name in fields_to_omit:
|
||||||
|
continue
|
||||||
serializer_field = serializer_class._declared_fields.get(field_name)
|
serializer_field = serializer_class._declared_fields.get(field_name)
|
||||||
|
|
||||||
# Determine the name of the model field referenced by the serializer field
|
# 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
|
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.
|
Return a mapping of field names to annotations to be applied to the queryset for a serializer.
|
||||||
"""
|
"""
|
||||||
annotations = {}
|
if fields is not None and omit is not None:
|
||||||
|
raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
|
||||||
# If specific fields are not specified, default to all
|
|
||||||
if not fields_to_include:
|
|
||||||
fields_to_include = serializer_class.Meta.fields
|
|
||||||
|
|
||||||
model = serializer_class.Meta.model
|
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():
|
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:
|
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
|
||||||
related_field = getattr(model, field.relation).field
|
related_field = getattr(model, field.relation).field
|
||||||
annotations[field_name] = count_related(related_field.model, related_field.name)
|
annotations[field_name] = count_related(related_field.model, related_field.name)
|
||||||
|
|||||||
Reference in New Issue
Block a user