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
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
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
@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
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
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
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
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
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
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
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
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.mixins 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
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
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
@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
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
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
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
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
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
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
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
@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
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
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
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
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}")