Skip to content

# `nautobot.apps.api`

Helpers for an app to implement a REST API.

## `nautobot.apps.api.BaseModelSerializer`

Bases: `OptInFieldsMixin`, `serializers.ModelSerializer`

This base serializer implements common fields and logic for all ModelSerializers.

Namely, it:

- defines the `display` field which exposes a human friendly value for the given object.
- ensures that `id` field is always present on the serializer as well
- ensures that `created` and `last_updated` fields are always present if applicable to this model and serializer.

Source code in `nautobot/core/api/serializers.py`

     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    class BaseModelSerializer(OptInFieldsMixin, serializers.ModelSerializer):
        """
        This base serializer implements common fields and logic for all ModelSerializers.

        Namely, it:

        - defines the `display` field which exposes a human friendly value for the given object.
        - ensures that `id` field is always present on the serializer as well
        - ensures that `created` and `last_updated` fields are always present if applicable to this model and serializer.
        """

        display = serializers.SerializerMethodField(read_only=True, help_text="Human friendly display value")

        @extend_schema_field(serializers.CharField)
        def get_display(self, instance):
            """
            Return either the `display` property of the instance or `str(instance)`
            """
            return getattr(instance, "display", str(instance))

        def extend_field_names(self, fields, field_name, at_start=False, opt_in_only=False):
            """Prepend or append the given field_name to `fields` and optionally self.Meta.opt_in_fields as well."""
            if field_name not in fields:
                if at_start:
                    fields.insert(0, field_name)
                else:
                    fields.append(field_name)
            if opt_in_only:
                if not getattr(self.Meta, "opt_in_fields", None):
                    self.Meta.opt_in_fields = [field_name]
                elif field_name not in self.Meta.opt_in_fields:
                    self.Meta.opt_in_fields.append(field_name)
            return fields

        def get_field_names(self, declared_fields, info):
            """
            Override get_field_names() to ensure certain fields are present even when not explicitly stated in Meta.fields.

            DRF does not automatically add declared fields to `Meta.fields`, nor does it require that declared fields
            on a super class be included in `Meta.fields` to allow for a subclass to include only a subset of declared
            fields from the super. This means either we intercept and ensure the fields at this level, or
            enforce by convention that all consumers of BaseModelSerializer include each of these standard fields in their
            `Meta.fields` which would surely lead to errors of omission; therefore we have chosen the former approach.

            Adds "id" and "display" to the start of `fields` for all models; also appends "created" and "last_updated"
            to the end of `fields` if they are applicable to this model and this is not a Nested serializer.
            """
            fields = list(super().get_field_names(declared_fields, info))  # Meta.fields could be defined as a tuple
            self.extend_field_names(fields, "display", at_start=True)
            self.extend_field_names(fields, "id", at_start=True)
            # Needed because we don't have a common base class for all nested serializers vs non-nested serializers
            if not self.__class__.__name__.startswith("Nested"):
                if hasattr(self.Meta.model, "created"):
                    self.extend_field_names(fields, "created")
                if hasattr(self.Meta.model, "last_updated"):
                    self.extend_field_names(fields, "last_updated")
            return fields

### `extend_field_names(fields, field_name, at_start=False, opt_in_only=False)`

Prepend or append the given field_name to `fields` and optionally self.Meta.opt_in_fields as well.

Source code in `nautobot/core/api/serializers.py`

    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    def extend_field_names(self, fields, field_name, at_start=False, opt_in_only=False):
        """Prepend or append the given field_name to `fields` and optionally self.Meta.opt_in_fields as well."""
        if field_name not in fields:
            if at_start:
                fields.insert(0, field_name)
            else:
                fields.append(field_name)
        if opt_in_only:
            if not getattr(self.Meta, "opt_in_fields", None):
                self.Meta.opt_in_fields = [field_name]
            elif field_name not in self.Meta.opt_in_fields:
                self.Meta.opt_in_fields.append(field_name)
        return fields

### `get_display(instance)`

Return either the `display` property of the instance or `str(instance)`

Source code in `nautobot/core/api/serializers.py`

     96
     97
     98
     99
    100
    101
    @extend_schema_field(serializers.CharField)
    def get_display(self, instance):
        """
        Return either the `display` property of the instance or `str(instance)`
        """
        return getattr(instance, "display", str(instance))

### `get_field_names(declared_fields, info)`

Override get_field_names() to ensure certain fields are present even when not explicitly stated in Meta.fields.

DRF does not automatically add declared fields to `Meta.fields`, nor does it require that declared fields on a super class be included in `Meta.fields` to allow for a subclass to include only a subset of declared fields from the super. This means either we intercept and ensure the fields at this level, or enforce by convention that all consumers of BaseModelSerializer include each of these standard fields in their `Meta.fields` which would surely lead to errors of omission; therefore we have chosen the former approach.

Adds "id" and "display" to the start of `fields` for all models; also appends "created" and "last_updated" to the end of `fields` if they are applicable to this model and this is not a Nested serializer.

Source code in `nautobot/core/api/serializers.py`

    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    def get_field_names(self, declared_fields, info):
        """
        Override get_field_names() to ensure certain fields are present even when not explicitly stated in Meta.fields.

        DRF does not automatically add declared fields to `Meta.fields`, nor does it require that declared fields
        on a super class be included in `Meta.fields` to allow for a subclass to include only a subset of declared
        fields from the super. This means either we intercept and ensure the fields at this level, or
        enforce by convention that all consumers of BaseModelSerializer include each of these standard fields in their
        `Meta.fields` which would surely lead to errors of omission; therefore we have chosen the former approach.

        Adds "id" and "display" to the start of `fields` for all models; also appends "created" and "last_updated"
        to the end of `fields` if they are applicable to this model and this is not a Nested serializer.
        """
        fields = list(super().get_field_names(declared_fields, info))  # Meta.fields could be defined as a tuple
        self.extend_field_names(fields, "display", at_start=True)
        self.extend_field_names(fields, "id", at_start=True)
        # Needed because we don't have a common base class for all nested serializers vs non-nested serializers
        if not self.__class__.__name__.startswith("Nested"):
            if hasattr(self.Meta.model, "created"):
                self.extend_field_names(fields, "created")
            if hasattr(self.Meta.model, "last_updated"):
                self.extend_field_names(fields, "last_updated")
        return fields

## `nautobot.apps.api.CustomFieldModelSerializerMixin`

Bases: `ValidatedModelSerializer`

Extends ModelSerializer to render any CustomFields and their values associated with an object.

Source code in `nautobot/extras/api/customfields.py`

     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    class CustomFieldModelSerializerMixin(ValidatedModelSerializer):
        """
        Extends ModelSerializer to render any CustomFields and their values associated with an object.
        """

        computed_fields = SerializerMethodField(read_only=True)
        custom_fields = CustomFieldsDataField(
            source="_custom_field_data",
            default=CreateOnlyDefault(CustomFieldDefaultValues()),
        )

        @extend_schema_field(OpenApiTypes.OBJECT)
        def get_computed_fields(self, obj):
            return obj.get_computed_fields()

        def get_field_names(self, declared_fields, info):
            """Ensure that "custom_fields" and "computed_fields" are always included appropriately."""
            fields = list(super().get_field_names(declared_fields, info))
            self.extend_field_names(fields, "custom_fields")
            self.extend_field_names(fields, "computed_fields", opt_in_only=True)
            return fields

### `get_field_names(declared_fields, info)`

Ensure that "custom_fields" and "computed_fields" are always included appropriately.

Source code in `nautobot/extras/api/customfields.py`

    107
    108
    109
    110
    111
    112
    def get_field_names(self, declared_fields, info):
        """Ensure that "custom_fields" and "computed_fields" are always included appropriately."""
        fields = list(super().get_field_names(declared_fields, info))
        self.extend_field_names(fields, "custom_fields")
        self.extend_field_names(fields, "computed_fields", opt_in_only=True)
        return fields

## `nautobot.apps.api.CustomFieldModelViewSet`

Bases: `ModelViewSet`

Include the applicable set of CustomFields in the ModelViewSet context.

Source code in `nautobot/extras/api/views.py`

    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    class CustomFieldModelViewSet(ModelViewSet):
        """
        Include the applicable set of CustomFields in the ModelViewSet context.
        """

        def get_serializer_context(self):
            # Gather all custom fields for the model
            content_type = ContentType.objects.get_for_model(self.queryset.model)
            custom_fields = content_type.custom_fields.all()

            context = super().get_serializer_context()
            context.update(
                {
                    "custom_fields": custom_fields,
                }
            )
            return context

## `nautobot.apps.api.ModelViewSet`

Bases: `NautobotAPIVersionMixin`, `BulkUpdateModelMixin`, `BulkDestroyModelMixin`, `ModelViewSetMixin`, `ModelViewSet_`

Extend DRF's ModelViewSet to support bulk update and delete functions.

Source code in `nautobot/core/api/views.py`

    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    class ModelViewSet(
        NautobotAPIVersionMixin,
        BulkUpdateModelMixin,
        BulkDestroyModelMixin,
        ModelViewSetMixin,
        ModelViewSet_,
    ):
        """
        Extend DRF's ModelViewSet to support bulk update and delete functions.
        """

        def _validate_objects(self, instance):
            """
            Check that the provided instance or list of instances are matched by the current queryset. This confirms that
            any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
            """
            if isinstance(instance, list):
                # Check that all instances are still included in the view's queryset
                conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
                if conforming_count != len(instance):
                    raise ObjectDoesNotExist
            else:
                # Check that the instance is matched by the view's queryset
                self.queryset.get(pk=instance.pk)

        def perform_create(self, serializer):
            model = self.queryset.model
            logger = logging.getLogger("nautobot.core.api.views.ModelViewSet")
            logger.info(f"Creating new {model._meta.verbose_name}")

            # Enforce object-level permissions on save()
            try:
                with transaction.atomic():
                    instance = serializer.save()
                    self._validate_objects(instance)
            except ObjectDoesNotExist:
                raise PermissionDenied()

        def perform_update(self, serializer):
            model = self.queryset.model
            logger = logging.getLogger("nautobot.core.api.views.ModelViewSet")
            logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})")

            # Enforce object-level permissions on save()
            try:
                with transaction.atomic():
                    instance = serializer.save()
                    self._validate_objects(instance)
            except ObjectDoesNotExist:
                raise PermissionDenied()

        def perform_destroy(self, instance):
            model = self.queryset.model
            logger = logging.getLogger("nautobot.core.api.views.ModelViewSet")
            logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")

            return super().perform_destroy(instance)

## `nautobot.apps.api.NautobotModelSerializer`

Bases: `RelationshipModelSerializerMixin`, `CustomFieldModelSerializerMixin`, `NotesSerializerMixin`, `ValidatedModelSerializer`

Base class to use for serializers based on OrganizationalModel or PrimaryModel.

Can also be used for models derived from BaseModel, so long as they support custom fields and relationships.

Source code in `nautobot/extras/api/serializers.py`

    170
    171
    172
    173
    174
    175
    176
    class NautobotModelSerializer(
        RelationshipModelSerializerMixin, CustomFieldModelSerializerMixin, NotesSerializerMixin, ValidatedModelSerializer
    ):
        """Base class to use for serializers based on OrganizationalModel or PrimaryModel.

        Can also be used for models derived from BaseModel, so long as they support custom fields and relationships.
        """

## `nautobot.apps.api.NautobotModelViewSet`

Bases: `CustomFieldModelViewSet`, `NotesViewSetMixin`

Base class to use for API ViewSets based on OrganizationalModel or PrimaryModel.

Can also be used for models derived from BaseModel, so long as they support Notes.

Source code in `nautobot/extras/api/views.py`

    302
    303
    304
    305
    306
    class NautobotModelViewSet(CustomFieldModelViewSet, NotesViewSetMixin):
        """Base class to use for API ViewSets based on OrganizationalModel or PrimaryModel.

        Can also be used for models derived from BaseModel, so long as they support Notes.
        """

## `nautobot.apps.api.NotesSerializerMixin`

Bases: `BaseModelSerializer`

Extend Serializer with a `notes` field.

Source code in `nautobot/extras/api/serializers.py`

    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    class NotesSerializerMixin(BaseModelSerializer):
        """Extend Serializer with a `notes` field."""

        notes_url = serializers.SerializerMethodField()

        def get_field_names(self, declared_fields, info):
            """Ensure that fields includes "notes_url" field if applicable."""
            fields = list(super().get_field_names(declared_fields, info))
            if hasattr(self.Meta.model, "notes"):
                self.extend_field_names(fields, "notes_url")
            return fields

        @extend_schema_field(serializers.URLField())
        def get_notes_url(self, instance):
            try:
                notes_url = get_route_for_model(instance, "notes", api=True)
                return reverse(notes_url, args=[instance.id], request=self.context["request"])
            except NoReverseMatch:
                model_name = type(instance).__name__
                logger.warning(
                    (
                        f"Notes feature is not available for model {model_name}. "
                        "Please make sure to: "
                        f"1. Include NotesMixin from nautobot.extras.model.mixins in the {model_name} class definition "
                        f"2. Include NotesViewSetMixin from nautobot.extras.api.views in the {model_name}ViewSet "
                        "before including NotesSerializerMixin in the model serializer"
                    )
                )

                return None

### `get_field_names(declared_fields, info)`

Ensure that fields includes "notes_url" field if applicable.

Source code in `nautobot/extras/api/serializers.py`

    143
    144
    145
    146
    147
    148
    def get_field_names(self, declared_fields, info):
        """Ensure that fields includes "notes_url" field if applicable."""
        fields = list(super().get_field_names(declared_fields, info))
        if hasattr(self.Meta.model, "notes"):
            self.extend_field_names(fields, "notes_url")
        return fields

## `nautobot.apps.api.NotesViewSetMixin`

Source code in `nautobot/extras/api/views.py`

     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    class NotesViewSetMixin:
        @extend_schema(methods=["get"], filters=False, responses={200: serializers.NoteSerializer(many=True)})
        @extend_schema(
            methods=["post"],
            request=serializers.NoteInputSerializer,
            responses={201: serializers.NoteSerializer(many=False)},
        )
        @action(detail=True, url_path="notes", methods=["get", "post"])
        def notes(self, request, pk=None):
            """
            API methods for returning or creating notes on an object.
            """
            obj = get_object_or_404(self.queryset, pk=pk)
            if request.method == "POST":
                content_type = ContentType.objects.get_for_model(obj)
                data = request.data
                data["assigned_object_id"] = obj.pk
                data["assigned_object_type"] = f"{content_type.app_label}.{content_type.model}"
                serializer = serializers.NoteSerializer(data=data, context={"request": request})

                # Create the new Note.
                serializer.is_valid(raise_exception=True)
                serializer.save(user=request.user)
                return Response(serializer.data, status=status.HTTP_201_CREATED)

            else:
                notes = self.paginate_queryset(obj.notes)
                serializer = serializers.NoteSerializer(notes, many=True, context={"request": request})

            return self.get_paginated_response(serializer.data)

### `notes(request, pk=None)`

API methods for returning or creating notes on an object.

Source code in `nautobot/extras/api/views.py`

     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    @extend_schema(methods=["get"], filters=False, responses={200: serializers.NoteSerializer(many=True)})
    @extend_schema(
        methods=["post"],
        request=serializers.NoteInputSerializer,
        responses={201: serializers.NoteSerializer(many=False)},
    )
    @action(detail=True, url_path="notes", methods=["get", "post"])
    def notes(self, request, pk=None):
        """
        API methods for returning or creating notes on an object.
        """
        obj = get_object_or_404(self.queryset, pk=pk)
        if request.method == "POST":
            content_type = ContentType.objects.get_for_model(obj)
            data = request.data
            data["assigned_object_id"] = obj.pk
            data["assigned_object_type"] = f"{content_type.app_label}.{content_type.model}"
            serializer = serializers.NoteSerializer(data=data, context={"request": request})

            # Create the new Note.
            serializer.is_valid(raise_exception=True)
            serializer.save(user=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        else:
            notes = self.paginate_queryset(obj.notes)
            serializer = serializers.NoteSerializer(notes, many=True, context={"request": request})

        return self.get_paginated_response(serializer.data)

## `nautobot.apps.api.OrderedDefaultRouter`

Bases: `DefaultRouter`

Source code in `nautobot/core/api/routers.py`

     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class OrderedDefaultRouter(DefaultRouter):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)

            # Extend the list view mappings to support the DELETE operation
            self.routes[0].mapping.update(
                {
                    "put": "bulk_update",
                    "patch": "bulk_partial_update",
                    "delete": "bulk_destroy",
                }
            )

        def get_api_root_view(self, api_urls=None):
            """
            Wrap DRF's DefaultRouter to return an alphabetized list of endpoints.
            """
            api_root_dict = OrderedDict()
            list_name = self.routes[0].name
            for prefix, _viewset, basename in sorted(self.registry, key=lambda x: x[0]):
                api_root_dict[prefix] = list_name.format(basename=basename)

            return self.APIRootView.as_view(api_root_dict=api_root_dict)

### `get_api_root_view(api_urls=None)`

Wrap DRF's DefaultRouter to return an alphabetized list of endpoints.

Source code in `nautobot/core/api/routers.py`

    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    def get_api_root_view(self, api_urls=None):
        """
        Wrap DRF's DefaultRouter to return an alphabetized list of endpoints.
        """
        api_root_dict = OrderedDict()
        list_name = self.routes[0].name
        for prefix, _viewset, basename in sorted(self.registry, key=lambda x: x[0]):
            api_root_dict[prefix] = list_name.format(basename=basename)

        return self.APIRootView.as_view(api_root_dict=api_root_dict)

## `nautobot.apps.api.ReadOnlyModelViewSet`

Bases: `NautobotAPIVersionMixin`, `ModelViewSetMixin`, `ReadOnlyModelViewSet_`

Extend DRF's ReadOnlyModelViewSet to support queryset restriction.

Source code in `nautobot/core/api/views.py`

    315
    316
    317
    318
    class ReadOnlyModelViewSet(NautobotAPIVersionMixin, ModelViewSetMixin, ReadOnlyModelViewSet_):
        """
        Extend DRF's ReadOnlyModelViewSet to support queryset restriction.
        """

## `nautobot.apps.api.RelationshipModelSerializerMixin`

Bases: `ValidatedModelSerializer`

Extend ValidatedModelSerializer with a `relationships` field.

Source code in `nautobot/extras/api/relationships.py`

    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    class RelationshipModelSerializerMixin(ValidatedModelSerializer):
        """Extend ValidatedModelSerializer with a `relationships` field."""

        relationships = RelationshipsDataField(required=False, source="*")

        def create(self, validated_data):
            relationships_data = validated_data.pop("relationships", {})
            required_relationships_errors = self.Meta().model.required_related_objects_errors(
                output_for="api", initial_data=relationships_data
            )
            if required_relationships_errors:
                raise ValidationError({"relationships": required_relationships_errors})
            instance = super().create(validated_data)
            if relationships_data:
                try:
                    self._save_relationships(instance, relationships_data)
                except DjangoValidationError as error:
                    raise ValidationError(str(error))
            return instance

        def update(self, instance, validated_data):
            relationships_key_specified = "relationships" in self.context["request"].data
            relationships_data = validated_data.pop("relationships", {})
            required_relationships_errors = self.Meta().model.required_related_objects_errors(
                output_for="api",
                initial_data=relationships_data,
                relationships_key_specified=relationships_key_specified,
                instance=instance,
            )
            if required_relationships_errors:
                raise ValidationError({"relationships": required_relationships_errors})

            instance = super().update(instance, validated_data)
            if relationships_data:
                self._save_relationships(instance, relationships_data)
            return instance

        def _save_relationships(self, instance, relationships):
            """Create/update RelationshipAssociations corresponding to a model instance."""
            # relationships has already passed RelationshipsDataField.to_internal_value(), so we can skip some try/excepts
            logger.debug("_save_relationships: %s : %s", instance, relationships)
            for relationship, relationship_data in relationships.items():
                for other_side in ["source", "destination", "peer"]:
                    if other_side not in relationship_data:
                        continue

                    other_type = getattr(relationship, f"{other_side}_type")
                    other_side_model = other_type.model_class()
                    other_side_serializer = get_serializer_for_model(other_side_model, prefix="Nested")
                    serializer_instance = other_side_serializer(context={"request": self.context.get("request")})

                    expected_objects_data = relationship_data[other_side]
                    expected_objects = [
                        serializer_instance.to_internal_value(object_data) for object_data in expected_objects_data
                    ]

                    this_side = RelationshipSideChoices.OPPOSITE[other_side]

                    if this_side != RelationshipSideChoices.SIDE_PEER:
                        existing_associations = relationship.associations.filter(**{f"{this_side}_id": instance.pk})
                        existing_objects = [assoc.get_peer(instance) for assoc in existing_associations]
                    else:
                        existing_associations_1 = relationship.associations.filter(source_id=instance.pk)
                        existing_objects_1 = [assoc.get_peer(instance) for assoc in existing_associations_1]
                        existing_associations_2 = relationship.associations.filter(destination_id=instance.pk)
                        existing_objects_2 = [assoc.get_peer(instance) for assoc in existing_associations_2]
                        existing_associations = list(existing_associations_1) + list(existing_associations_2)
                        existing_objects = existing_objects_1 + existing_objects_2

                    add_objects = []
                    remove_assocs = []

                    for obj, assoc in zip(existing_objects, existing_associations):
                        if obj not in expected_objects:
                            remove_assocs.append(assoc)
                    for obj in expected_objects:
                        if obj not in existing_objects:
                            add_objects.append(obj)

                    for add_object in add_objects:
                        if "request" in self.context and not self.context["request"].user.has_perm(
                            "extras.add_relationshipassociation"
                        ):
                            raise PermissionDenied("This user does not have permission to create RelationshipAssociations.")
                        if other_side != RelationshipSideChoices.SIDE_SOURCE:
                            assoc = RelationshipAssociation(
                                relationship=relationship,
                                source_type=relationship.source_type,
                                source_id=instance.id,
                                destination_type=relationship.destination_type,
                                destination_id=add_object.id,
                            )
                        else:
                            assoc = RelationshipAssociation(
                                relationship=relationship,
                                source_type=relationship.source_type,
                                source_id=add_object.id,
                                destination_type=relationship.destination_type,
                                destination_id=instance.id,
                            )
                        assoc.validated_save()  # enforce relationship filter logic, etc.
                        logger.debug("Created %s", assoc)

                    for remove_assoc in remove_assocs:
                        if "request" in self.context and not self.context["request"].user.has_perm(
                            "extras.delete_relationshipassociation"
                        ):
                            raise PermissionDenied("This user does not have permission to delete RelationshipAssociations.")
                        logger.debug("Deleting %s", remove_assoc)
                        remove_assoc.delete()

        def get_field_names(self, declared_fields, info):
            """Ensure that "relationships" is always included as an opt-in field."""
            fields = list(super().get_field_names(declared_fields, info))
            self.extend_field_names(fields, "relationships", opt_in_only=True)
            return fields

### `get_field_names(declared_fields, info)`

Ensure that "relationships" is always included as an opt-in field.

Source code in `nautobot/extras/api/relationships.py`

    374
    375
    376
    377
    378
    def get_field_names(self, declared_fields, info):
        """Ensure that "relationships" is always included as an opt-in field."""
        fields = list(super().get_field_names(declared_fields, info))
        self.extend_field_names(fields, "relationships", opt_in_only=True)
        return fields

## `nautobot.apps.api.StatusModelSerializerMixin`

Bases: `BaseModelSerializer`

Mixin to add `status` choice field to model serializers.

Source code in `nautobot/extras/api/serializers.py`

    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    class StatusModelSerializerMixin(BaseModelSerializer):
        """Mixin to add `status` choice field to model serializers."""

        status = StatusSerializerField(queryset=Status.objects.all())

        def get_field_names(self, declared_fields, info):
            """Ensure that "status" field is always present."""
            fields = list(super().get_field_names(declared_fields, info))
            self.extend_field_names(fields, "status")
            return fields

        @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
        def status_choices(cls):  # pylint: disable=no-self-argument
            """
            Get the list of valid status values for this serializer.

            In the case where multiple serializers have the same set of status choices, it's necessary to set
            settings.SPECTACULAR_SETTINGS["ENUM_NAME_OVERRIDES"] for at least one of the matching serializers,
            or else drf-spectacular will report:
            'enum naming encountered a non-optimally resolvable collision for fields named "status"'
            """
            return list(cls().fields["status"].get_choices().keys())

### `get_field_names(declared_fields, info)`

Ensure that "status" field is always present.

Source code in `nautobot/extras/api/serializers.py`

    184
    185
    186
    187
    188
    def get_field_names(self, declared_fields, info):
        """Ensure that "status" field is always present."""
        fields = list(super().get_field_names(declared_fields, info))
        self.extend_field_names(fields, "status")
        return fields

### `status_choices()`

Get the list of valid status values for this serializer.

In the case where multiple serializers have the same set of status choices, it's necessary to set settings.SPECTACULAR_SETTINGS["ENUM_NAME_OVERRIDES"] for at least one of the matching serializers, or else drf-spectacular will report: 'enum naming encountered a non-optimally resolvable collision for fields named "status"'

Source code in `nautobot/extras/api/serializers.py`

    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
    def status_choices(cls):  # pylint: disable=no-self-argument
        """
        Get the list of valid status values for this serializer.

        In the case where multiple serializers have the same set of status choices, it's necessary to set
        settings.SPECTACULAR_SETTINGS["ENUM_NAME_OVERRIDES"] for at least one of the matching serializers,
        or else drf-spectacular will report:
        'enum naming encountered a non-optimally resolvable collision for fields named "status"'
        """
        return list(cls().fields["status"].get_choices().keys())

## `nautobot.apps.api.TaggedModelSerializerMixin`

Bases: `BaseModelSerializer`

Source code in `nautobot/extras/api/serializers.py`

    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    class TaggedModelSerializerMixin(BaseModelSerializer):
        tags = TagSerializerField(many=True, required=False)

        def get_field_names(self, declared_fields, info):
            """Ensure that 'tags' field is always present."""
            fields = list(super().get_field_names(declared_fields, info))
            self.extend_field_names(fields, "tags")
            return fields

        def create(self, validated_data):
            tags = validated_data.pop("tags", None)
            instance = super().create(validated_data)

            if tags is not None:
                return self._save_tags(instance, tags)
            return instance

        def update(self, instance, validated_data):
            tags = validated_data.pop("tags", None)

            # Cache tags on instance for change logging
            instance._tags = tags or []

            instance = super().update(instance, validated_data)

            if tags is not None:
                return self._save_tags(instance, tags)
            return instance

        def _save_tags(self, instance, tags):
            if tags:
                instance.tags.set([t.name for t in tags])
            else:
                instance.tags.clear()

            return instance

### `get_field_names(declared_fields, info)`

Ensure that 'tags' field is always present.

Source code in `nautobot/extras/api/serializers.py`

    217
    218
    219
    220
    221
    def get_field_names(self, declared_fields, info):
        """Ensure that 'tags' field is always present."""
        fields = list(super().get_field_names(declared_fields, info))
        self.extend_field_names(fields, "tags")
        return fields

## `nautobot.apps.api.ValidatedModelSerializer`

Bases: `BaseModelSerializer`

Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)

Source code in `nautobot/core/api/serializers.py`

    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    class ValidatedModelSerializer(BaseModelSerializer):
        """
        Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during
        validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
        """

        def validate(self, data):
            # Remove custom fields data and tags (if any) prior to model validation
            attrs = data.copy()
            attrs.pop("custom_fields", None)
            attrs.pop("relationships", None)
            attrs.pop("tags", None)

            # Skip ManyToManyFields
            for field in self.Meta.model._meta.get_fields():
                if isinstance(field, ManyToManyField):
                    attrs.pop(field.name, None)

            # Run clean() on an instance of the model
            if self.instance is None:
                instance = self.Meta.model(**attrs)
            else:
                instance = self.instance
                for k, v in attrs.items():
                    setattr(instance, k, v)
            instance.full_clean()

            return data

## `nautobot.apps.api.WritableNestedSerializer`

Bases: `BaseModelSerializer`

Returns a nested representation of an object on read, but accepts either the nested representation or the primary key value on write operations.

Source code in `nautobot/core/api/serializers.py`

    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    class WritableNestedSerializer(BaseModelSerializer):
        """
        Returns a nested representation of an object on read, but accepts either the nested representation or the
        primary key value on write operations.
        """

        def get_queryset(self):
            return self.Meta.model.objects

        def to_internal_value(self, data):
            if data is None:
                return None

            # Dictionary of related object attributes
            if isinstance(data, dict):
                params = dict_to_filter_params(data)

                # Make output from a WritableNestedSerializer "round-trip" capable by automatically stripping from the
                # data any serializer fields that do not correspond to a specific model field
                for field_name, field_instance in self.fields.items():
                    if field_name in params and field_instance.source == "*":
                        logger.debug("Discarding non-database field %s", field_name)
                        del params[field_name]

                queryset = self.get_queryset()
                try:
                    return queryset.get(**params)
                except ObjectDoesNotExist:
                    raise ValidationError(f"Related object not found using the provided attributes: {params}")
                except MultipleObjectsReturned:
                    raise ValidationError(f"Multiple objects match the provided attributes: {params}")
                except FieldError as e:
                    raise ValidationError(e)

            queryset = self.get_queryset()
            pk = None

            if isinstance(self.Meta.model._meta.pk, AutoField):
                # PK is an int for this model. This is usually the User model
                try:
                    pk = int(data)
                except (TypeError, ValueError):
                    raise ValidationError(
                        "Related objects must be referenced by ID or by dictionary of attributes. Received an "
                        f"unrecognized value: {data}"
                    )

            else:
                # We assume a type of UUIDField for all other models

                # PK of related object
                try:
                    # Ensure the pk is a valid UUID
                    pk = uuid.UUID(str(data))
                except (TypeError, ValueError):
                    raise ValidationError(
                        "Related objects must be referenced by ID or by dictionary of attributes. Received an "
                        f"unrecognized value: {data}"
                    )

            try:
                return queryset.get(pk=pk)
            except ObjectDoesNotExist:
                raise ValidationError(f"Related object not found using the provided ID: {pk}")