Coverage for netbox/api/serializers/base.py: 100%
68 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from functools import cached_property
3from drf_spectacular.types import OpenApiTypes
4from drf_spectacular.utils import extend_schema_field
5from rest_framework import serializers
7from netbox.api.fields import RelatedObjectCountField
8from utilities.api import get_related_object_by_attrs
10from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
12__all__ = (
13 'BaseModelSerializer',
14 'ValidatedModelSerializer',
15)
18class BaseModelSerializer(serializers.ModelSerializer):
19 url = NetBoxAPIHyperlinkedIdentityField()
20 display_url = NetBoxURLHyperlinkedIdentityField()
21 display = serializers.SerializerMethodField(read_only=True)
23 def __init__(self, *args, nested=False, fields=None, omit=None, **kwargs):
24 """
25 Extends the base __init__() method to support dynamic fields.
27 :param nested: Set to True if this serializer is being employed within a parent serializer
28 :param fields: An iterable of fields to include when rendering the serialized object, If nested is
29 True but no fields are specified, Meta.brief_fields will be used.
30 :param omit: An iterable of fields to omit from the serialized object
31 """
32 self.nested = nested
33 self._include_fields = fields or []
34 self._omit_fields = omit or []
36 # Disable validators for nested objects (which already exist)
37 if self.nested:
38 self.validators = []
40 # If this serializer is nested but no fields have been specified,
41 # default to using Meta.brief_fields (if set)
42 if self.nested and not fields and not omit:
43 self._include_fields = getattr(self.Meta, 'brief_fields', None)
45 super().__init__(*args, **kwargs)
47 def to_internal_value(self, data):
49 # If initialized as a nested serializer, we should expect to receive the attrs or PK
50 # identifying a related object.
51 if self.nested:
52 queryset = self.Meta.model.objects.all()
53 request = self.context.get('request')
54 user = request.user if request else None
55 return get_related_object_by_attrs(queryset, data, user=user)
57 return super().to_internal_value(data)
59 @cached_property
60 def fields(self):
61 """
62 Override the fields property to return only specifically requested fields if needed.
63 """
64 fields = super().fields
66 # Include only requested fields
67 if self._include_fields:
68 for field_name in set(fields) - set(self._include_fields):
69 fields.pop(field_name, None)
71 # Remove omitted fields
72 for field_name in set(self._omit_fields):
73 fields.pop(field_name, None)
75 # Related object counts are populated by annotations applied to the viewset's queryset, but these
76 # annotations are not applied when the object is represented as a nested (brief) related object. Omit
77 # these fields when serializing a nested object to avoid advertising fields that will never be populated
78 # (and which would otherwise be declared as required in the generated OpenAPI schema). See #22154.
79 if self.nested:
80 for field_name, field in list(fields.items()):
81 if isinstance(field, RelatedObjectCountField):
82 fields.pop(field_name)
84 return fields
86 @extend_schema_field(OpenApiTypes.STR)
87 def get_display(self, obj):
88 return str(obj)
91class ValidatedModelSerializer(BaseModelSerializer):
92 """
93 Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during
94 validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
96 Serializers may declare `model_clean_fields` in Meta, naming scalar model fields whose clean()-normalized values
97 must reach the database. DRF builds a fresh instance from the validated data on create, discarding what clean()
98 wrote.
99 """
101 # Bypass DRF's built-in validation of unique constraints due to DRF bug #9410. Rely instead
102 # on our own custom model validation (below).
103 def get_unique_together_constraints(self, model):
104 return []
106 def validate(self, data):
108 # Skip validation if we're being used to represent a nested object
109 if self.nested:
110 return data
112 attrs = data.copy()
114 # Skip ManyToManyFields
115 opts = self.Meta.model._meta
116 m2m_values = {}
117 for field in [*opts.local_many_to_many, *opts.related_objects]:
118 if field.name in attrs:
119 m2m_values[field.name] = attrs.pop(field.name)
121 # Run clean() on an instance of the model
122 if self.instance is None:
123 instance = self.Meta.model(**attrs)
124 else:
125 instance = self.instance
126 for k, v in attrs.items():
127 setattr(instance, k, v)
128 instance._m2m_values = m2m_values
129 # Skip uniqueness validation of individual fields inside `full_clean()` (this is handled by the serializer)
130 instance.full_clean(validate_unique=False)
132 # Preserve any normalization performed by model.clean() (e.g. stale custom field pruning)
133 if 'custom_field_data' in attrs:
134 data['custom_field_data'] = instance.custom_field_data
136 for field_name in getattr(self.Meta, 'model_clean_fields', ()):
137 data[field_name] = getattr(instance, field_name)
139 return data