Skip to content

Implementing Custom Validators

Apps can register custom validator classes which implement model validation logic to be executed during a model's clean() method. Like template extensions, custom validators are registered to a single model and offer a method which app authors override to implement their validation logic. This is accomplished by subclassing CustomValidator and implementing the clean() method.

App authors must raise django.core.exceptions.ValidationError within the clean() method to trigger validation error messages which are propagated to the user and prevent saving of the model instance. A convenience method validation_error() may be used to simplify this process. Raising a ValidationError is no different than vanilla Django, and the convenience method will simply pass the provided message through to the exception.

When a CustomValidator is instantiated, the model instance is assigned to context dictionary using the object key, much like TemplateExtension. E.g. self.context['object']. The context is also populated with the current request user object. E.g. self.context['user'].

Declared subclasses should be gathered into a list or tuple for integration with Nautobot. By default, Nautobot looks for an iterable named custom_validators within a custom_validators.py file. (This can be overridden by setting custom_validators to a custom value on the app's NautobotAppConfig.) An example is below.

# custom_validators.py
from nautobot.apps.models import CustomValidator


class LocationValidator(CustomValidator):
    """Custom validator for Locations to enforce that they must have a Tenant."""

    model = 'dcim.location'

    def clean(self):
        if self.context['object'].tenant is None:
            # Enforce that all locations must have a tenant
            self.validation_error({
                "tenant": "All locations must have a tenant"
            })


custom_validators = [LocationValidator]

User Context

Added in version 2.4.4

Custom validators have access to the current user via self.context['user'] whenever the custom validator is invoked within a web request context. This is true for any Web UI, REST API request, Job execution, and anytime the web_request_context context manager is used in nbshell or a out of band script. In the event a custom validator is run outside of a web request context, self.context['user'] will be populated with an instance of AnonymousUser from django.contrib.auth. This allows a custom validator author to write code that is durable to cases where a real user is not available.

With the user object, you can inspect the groups and permissions that the user has, allowing more granular access related validation.

This example shows a custom validator that only allows users in the group "Tenant Managers" to change the tenant of a location:

# custom_validators.py
from nautobot.apps.models import CustomValidator


class LocationTenantValidator(CustomValidator):
    """Custom validator for Locations to enforce that only some users can update the Tenant."""

    model = 'dcim.location'

    def clean(self):
        new_object_state = self.context["object"]
        if not new_object_state.present_in_database:
            # This is a brand new Location, so skip the rest of the checks here
            return

        # Get a copy of the object as it currently exists in the database
        current_object_state = new_object_state._meta.model.objects.get(id=new_object_state.id)

        # Compare the tenant values between the two states
        if new_object_state.tenant != current_object_state.tenant:

            # Check if the user has permission to change the tenant, via being a member of "Tenant Managers"
            if not self.context["user"].groups.filter(name="Tenant Managers").exists():
                self.validation_error({
                    "tenant": "You do not have permission to change the tenant"
                })


custom_validators = [LocationTenantValidator]