Skip to content

nautobot.apps.api

Helpers for an app to implement a REST API.

nautobot.apps.api.BaseModelSerializer

Bases: OptInFieldsMixin, serializers.HyperlinkedModelSerializer

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.
  • ensures that object_type field is always present on the serializer which represents the content-type of this serializer's associated model (e.g. "dcim.device"). This is required as the OpenAPI schema, using the PolymorphicProxySerializer class defined below, relies upon this field as a way to identify to the client which of several possible serializers are in use for a given attribute.
  • supports ?depth query parameter. It is passed in as nested_depth to the build_nested_field() function to enable the dynamic generation of nested serializers.
Source code in nautobot/core/api/serializers.py
class BaseModelSerializer(OptInFieldsMixin, serializers.HyperlinkedModelSerializer):
    """
    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.
    - ensures that `object_type` field is always present on the serializer which represents the content-type of this
      serializer's associated model (e.g. "dcim.device"). This is required as the OpenAPI schema, using the
      PolymorphicProxySerializer class defined below, relies upon this field as a way to identify to the client
      which of several possible serializers are in use for a given attribute.
    - supports `?depth` query parameter. It is passed in as `nested_depth` to the `build_nested_field()` function
      to enable the dynamic generation of nested serializers.
    """

    serializer_related_field = NautobotHyperlinkedRelatedField

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

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # If it is not a Nested Serializer, we should set the depth argument to whatever is in the request's context
        if not self.is_nested:
            self.Meta.depth = self.context.get("depth", 0)

    @property
    def is_nested(self):
        """Return whether this is a nested serializer."""
        return getattr(self.Meta, "is_nested", False)

    @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))

    @extend_schema_field(
        {
            "type": "string",
            "example": COMPOSITE_KEY_SEPARATOR.join(["attribute1", "attribute2"]),
        }
    )
    def get_composite_key(self, instance):
        try:
            return getattr(instance, "composite_key", construct_composite_key(instance.natural_key()))
        except (AttributeError, NotImplementedError):
            return "unknown"

    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 in fields:
            fields.remove(field_name)
        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 add some custom logic.

        Assuming that we follow the pattern where `fields = "__all__" for the vast majority of serializers in Nautobot,
        we do not strictly need to use this method to protect against inadvertently omitting standard fields
        like `display`, `created`, and `last_updated`. However, we continue to do as a bit of redundant safety.

        The other purpose of this method now is to manipulate the set of fields that "__all__" actually means as a
        way of *excluding* fields that we *don't* want to include by default for performance or data exposure reasons.
        """
        fields = list(super().get_field_names(declared_fields, info))  # Meta.fields could be defined as a tuple

        # Add initial fields in "reverse" order since they're each inserted at the start of the list.
        self.extend_field_names(fields, "display", at_start=True)
        self.extend_field_names(fields, "object_type", at_start=True)
        # Since we use HyperlinkedModelSerializer as our base class, "url" is auto-included by "__all__" but "id" isn't.
        self.extend_field_names(fields, "id", at_start=True)

        # Move these fields to the end
        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")

        def filter_field(field):
            # Eliminate all field names that start with "_" as those fields are not user-facing
            if field.startswith("_"):
                return False
            # These are expensive to look up, so we have decided not to include them on nested serializers
            if self.is_nested and isinstance(getattr(self.Meta.model, field, None), ManyToManyDescriptor):
                return False
            return True

        fields = [field for field in fields if filter_field(field)]
        return fields

    def build_field(self, field_name, info, model_class, nested_depth):
        """
        Return a two tuple of (cls, kwargs) to build a serializer field with.
        """
        request = self.context.get("request")
        # Make sure that PATCH/POST/PUT method response serializers are consistent
        # with depth of 0
        if request is not None and request.method != "GET":
            nested_depth = 0
        # For tags field, DRF does not recognize the relationship between tags and the model itself (?)
        # so instead of calling build_nested_field() it will call build_property_field() which
        # makes the field impervious to the `?depth` parameter.
        # So we intercept it here to call build_nested_field()
        # which will make the tags field be rendered with TagSerializer() and respect the `depth` parameter.
        if isinstance(getattr(model_class, field_name, None), TagsManager) and nested_depth > 0:
            tags_field = getattr(model_class, field_name)
            relation_info = RelationInfo(
                model_field=tags_field,
                related_model=Tag,
                to_many=True,
                has_through_model=True,
                to_field=_get_to_field(tags_field),
                reverse=False,
            )
            return self.build_nested_field(field_name, relation_info, nested_depth)

        return super().build_field(field_name, info, model_class, nested_depth)

    def build_relational_field(self, field_name, relation_info):
        """Override DRF's default relational-field construction to be app-aware."""
        field_class, field_kwargs = super().build_relational_field(field_name, relation_info)
        if "view_name" in field_kwargs:
            field_kwargs["view_name"] = get_route_for_model(relation_info.related_model, "detail", api=True)
        return field_class, field_kwargs

    def build_property_field(self, field_name, model_class):
        """
        Create a property field for model methods and properties.
        """
        if isinstance(getattr(model_class, field_name, None), TagsManager):
            field_class = NautobotHyperlinkedRelatedField
            field_kwargs = {
                "queryset": Tag.objects.get_for_model(model_class),
                "many": True,
                "required": False,
            }

            return field_class, field_kwargs
        return super().build_property_field(field_name, model_class)

    def build_nested_field(self, field_name, relation_info, nested_depth):
        return nested_serializer_factory(relation_info, nested_depth)

    def build_url_field(self, field_name, model_class):
        """Override DRF's default 'url' field construction to be app-aware."""
        field_class, field_kwargs = super().build_url_field(field_name, model_class)
        if "view_name" in field_kwargs:
            field_kwargs["view_name"] = get_route_for_model(model_class, "detail", api=True)
        return field_class, field_kwargs

is_nested property

Return whether this is a nested serializer.

build_field(field_name, info, model_class, nested_depth)

Return a two tuple of (cls, kwargs) to build a serializer field with.

Source code in nautobot/core/api/serializers.py
def build_field(self, field_name, info, model_class, nested_depth):
    """
    Return a two tuple of (cls, kwargs) to build a serializer field with.
    """
    request = self.context.get("request")
    # Make sure that PATCH/POST/PUT method response serializers are consistent
    # with depth of 0
    if request is not None and request.method != "GET":
        nested_depth = 0
    # For tags field, DRF does not recognize the relationship between tags and the model itself (?)
    # so instead of calling build_nested_field() it will call build_property_field() which
    # makes the field impervious to the `?depth` parameter.
    # So we intercept it here to call build_nested_field()
    # which will make the tags field be rendered with TagSerializer() and respect the `depth` parameter.
    if isinstance(getattr(model_class, field_name, None), TagsManager) and nested_depth > 0:
        tags_field = getattr(model_class, field_name)
        relation_info = RelationInfo(
            model_field=tags_field,
            related_model=Tag,
            to_many=True,
            has_through_model=True,
            to_field=_get_to_field(tags_field),
            reverse=False,
        )
        return self.build_nested_field(field_name, relation_info, nested_depth)

    return super().build_field(field_name, info, model_class, nested_depth)

build_property_field(field_name, model_class)

Create a property field for model methods and properties.

Source code in nautobot/core/api/serializers.py
def build_property_field(self, field_name, model_class):
    """
    Create a property field for model methods and properties.
    """
    if isinstance(getattr(model_class, field_name, None), TagsManager):
        field_class = NautobotHyperlinkedRelatedField
        field_kwargs = {
            "queryset": Tag.objects.get_for_model(model_class),
            "many": True,
            "required": False,
        }

        return field_class, field_kwargs
    return super().build_property_field(field_name, model_class)

build_relational_field(field_name, relation_info)

Override DRF's default relational-field construction to be app-aware.

Source code in nautobot/core/api/serializers.py
def build_relational_field(self, field_name, relation_info):
    """Override DRF's default relational-field construction to be app-aware."""
    field_class, field_kwargs = super().build_relational_field(field_name, relation_info)
    if "view_name" in field_kwargs:
        field_kwargs["view_name"] = get_route_for_model(relation_info.related_model, "detail", api=True)
    return field_class, field_kwargs

build_url_field(field_name, model_class)

Override DRF's default 'url' field construction to be app-aware.

Source code in nautobot/core/api/serializers.py
def build_url_field(self, field_name, model_class):
    """Override DRF's default 'url' field construction to be app-aware."""
    field_class, field_kwargs = super().build_url_field(field_name, model_class)
    if "view_name" in field_kwargs:
        field_kwargs["view_name"] = get_route_for_model(model_class, "detail", api=True)
    return field_class, field_kwargs

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 in fields:
        fields.remove(field_name)
    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 add some custom logic.

Assuming that we follow the pattern where fields = "__all__" for the vast majority of serializers in Nautobot, we do not strictly need to use this method to protect against inadvertently omitting standard fields likedisplay,created, andlast_updated`. However, we continue to do as a bit of redundant safety.

The other purpose of this method now is to manipulate the set of fields that "all" actually means as a way of excluding fields that we don't want to include by default for performance or data exposure reasons.

Source code in nautobot/core/api/serializers.py
def get_field_names(self, declared_fields, info):
    """
    Override get_field_names() to add some custom logic.

    Assuming that we follow the pattern where `fields = "__all__" for the vast majority of serializers in Nautobot,
    we do not strictly need to use this method to protect against inadvertently omitting standard fields
    like `display`, `created`, and `last_updated`. However, we continue to do as a bit of redundant safety.

    The other purpose of this method now is to manipulate the set of fields that "__all__" actually means as a
    way of *excluding* fields that we *don't* want to include by default for performance or data exposure reasons.
    """
    fields = list(super().get_field_names(declared_fields, info))  # Meta.fields could be defined as a tuple

    # Add initial fields in "reverse" order since they're each inserted at the start of the list.
    self.extend_field_names(fields, "display", at_start=True)
    self.extend_field_names(fields, "object_type", at_start=True)
    # Since we use HyperlinkedModelSerializer as our base class, "url" is auto-included by "__all__" but "id" isn't.
    self.extend_field_names(fields, "id", at_start=True)

    # Move these fields to the end
    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")

    def filter_field(field):
        # Eliminate all field names that start with "_" as those fields are not user-facing
        if field.startswith("_"):
            return False
        # These are expensive to look up, so we have decided not to include them on nested serializers
        if self.is_nested and isinstance(getattr(self.Meta.model, field, None), ManyToManyDescriptor):
            return False
        return True

    fields = [field for field in fields if filter_field(field)]
    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/core/api/serializers.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 included appropriately."""
        fields = list(super().get_field_names(declared_fields, info))
        # Ensure that custom_fields field appears at the end, not the start, of the fields
        self.extend_field_names(fields, "custom_fields")
        if not self.is_nested:
            # Only include computed_fields as opt-in.
            self.extend_field_names(fields, "computed_fields", opt_in_only=True)
        else:
            # As computed fields are expensive, do not include them in nested serializers even if opted-in at the root
            if "computed_fields" in fields:
                fields.remove("computed_fields")
        return fields

get_field_names(declared_fields, info)

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

Source code in nautobot/core/api/serializers.py
def get_field_names(self, declared_fields, info):
    """Ensure that "custom_fields" and "computed_fields" are included appropriately."""
    fields = list(super().get_field_names(declared_fields, info))
    # Ensure that custom_fields field appears at the end, not the start, of the fields
    self.extend_field_names(fields, "custom_fields")
    if not self.is_nested:
        # Only include computed_fields as opt-in.
        self.extend_field_names(fields, "computed_fields", opt_in_only=True)
    else:
        # As computed fields are expensive, do not include them in nested serializers even if opted-in at the root
        if "computed_fields" in fields:
            fields.remove("computed_fields")
    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.
    """

    logger = logging.getLogger(__name__ + ".ModelViewSet")

    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
        self.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
        self.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
        self.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, notes, and relationships.

Source code in nautobot/core/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, notes, 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/core/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"):
            # Make sure the field is at the end of fields, instead of the beginning
            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/core/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"):
        # Make sure the field is at the end of fields, instead of the beginning
        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/core/api/serializers.py
class RelationshipModelSerializerMixin(ValidatedModelSerializer):
    """Extend ValidatedModelSerializer with a `relationships` field."""

    # TODO # 3024 need to change this as well to show just pks in depth=0
    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)) from 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:
            try:
                self._save_relationships(instance, relationships_data)
            except DjangoValidationError as error:
                raise ValidationError(str(error)) from error
        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()

                expected_objects_data = relationship_data[other_side]
                expected_objects = [
                    other_side_model.objects.get(**object_data) for object_data in expected_objects_data
                ]

                this_side = RelationshipSideChoices.OPPOSITE[other_side]

                if this_side != RelationshipSideChoices.SIDE_PEER:
                    existing_associations = relationship.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.relationship_associations.filter(source_id=instance.pk)
                    existing_objects_1 = [assoc.get_peer(instance) for assoc in existing_associations_1]
                    existing_associations_2 = relationship.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 included as an opt-in field on root serializers."""
        fields = list(super().get_field_names(declared_fields, info))
        if not self.is_nested:
            # Only include relationships as opt-in.
            self.extend_field_names(fields, "relationships", opt_in_only=True)
        else:
            # As relationships are expensive, do not include them on nested serializers even if opted in.
            if "relationships" in fields:
                fields.remove("relationships")
        return fields

get_field_names(declared_fields, info)

Ensure that "relationships" is included as an opt-in field on root serializers.

Source code in nautobot/core/api/serializers.py
def get_field_names(self, declared_fields, info):
    """Ensure that "relationships" is included as an opt-in field on root serializers."""
    fields = list(super().get_field_names(declared_fields, info))
    if not self.is_nested:
        # Only include relationships as opt-in.
        self.extend_field_names(fields, "relationships", opt_in_only=True)
    else:
        # As relationships are expensive, do not include them on nested serializers even if opted in.
        if "relationships" in fields:
            fields.remove("relationships")
    return fields

nautobot.apps.api.TaggedModelSerializerMixin

Bases: BaseModelSerializer

Source code in nautobot/extras/api/mixins.py
class TaggedModelSerializerMixin(BaseModelSerializer):
    def get_field_names(self, declared_fields, info):
        """Ensure that 'tags' field is always present except on nested serializers."""
        fields = list(super().get_field_names(declared_fields, info))
        if not self.is_nested:
            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 except on nested serializers.

Source code in nautobot/extras/api/mixins.py
def get_field_names(self, declared_fields, info):
    """Ensure that 'tags' field is always present except on nested serializers."""
    fields = list(super().get_field_names(declared_fields, info))
    if not self.is_nested:
        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.

Note that subclasses will always have a read-only object_type field, which represents the content-type of this serializer's associated model (e.g. "dcim.device"). This is required as the OpenAPI schema, using the PolymorphicProxySerializer class defined below, relies upon this field as a way to identify to the client which of several possible nested serializers are in use for a given attribute.

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.

    Note that subclasses will always have a read-only `object_type` field, which represents the content-type of this
    serializer's associated model (e.g. "dcim.device"). This is required as the OpenAPI schema, using the
    PolymorphicProxySerializer class defined below, relies upon this field as a way to identify to the client
    which of several possible nested serializers are in use for a given attribute.
    """

    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}")