Skip to content

API Swagger crashes when a model has a single‑field UniqueConstraint without a condition (regression in monkey‑patched DRF get_unique_validators()) #20585

@pheus

Description

@pheus

NetBox Edition

NetBox Community

NetBox Version

v4.4.3

Python Version

3.10

Steps to Reproduce

Hi team, and thanks for all the work that went into #19302. After updating to v4.4.3, I’m seeing a new issue that appears tied to the recent monkey‑patch of DRF’s get_unique_validators().

Generating the OpenAPI schema at /api/schema/ returns 500 Internal Server Error if a (core or plugin) model defines a single‑field UniqueConstraint without an explicit condition (condition=None). This started after the change that monkey‑patches DRF’s get_unique_validators() in v4.4.3.

The patched function collects UniqueConstraint.condition values and then filters them by comparing cond.referenced_base_fields to the base field set. For unconditional constraints, cond is None, so accessing cond.referenced_base_fields raises an AttributeError. That makes /api/schema/ unusable when such constraints exist.

  1. Run NetBox v4.4.3.
  2. Include a model that defines a single‑field UniqueConstraint with no condition (e.g., in a plugin model).
  3. Visit /api/schema/.

Example (minimal model)

from django.db import models

class Example(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name"],
                name="example_name_unique"  # no condition -> condition=None
            )
        ]

Expected Behavior

The OpenAPI schema at /api/schema/ renders successfully.

Observed Behavior

/api/schema/ returns 500 with an exception from the monkey‑patched get_unique_validators():

AttributeError: 'NoneType' object has no attribute 'referenced_base_fields'

Full traceback (from my environment)

Internal Server Error: /api/schema/
Traceback (most recent call last):
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/utils/decorators.py", line 192, in _view_wrapper
    result = _process_exception(request, e)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/utils/decorators.py", line 190, in _view_wrapper
    response = view_func(request, *args, **kwargs)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 65, in _view_wrapper
    return view_func(request, *args, **kwargs)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/views/generic/base.py", line 105, in view
    return self.dispatch(request, *args, **kwargs)
  File "/opt/netbox/venv/lib/python3.10/site-packages/rest_framework/views.py", line 515, in dispatch
    response = self.handle_exception(exc)
  File "/opt/netbox/venv/lib/python3.10/site-packages/rest_framework/views.py", line 475, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/opt/netbox/venv/lib/python3.10/site-packages/rest_framework/views.py", line 486, in raise_uncaught_exception
    raise exc
  File "/opt/netbox/venv/lib/python3.10/site-packages/rest_framework/views.py", line 512, in dispatch
    response = handler(request, *args, **kwargs)
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/views.py", line 84, in get
    return self._get_schema_response(request)
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/views.py", line 92, in _get_schema_response
    data=generator.get_schema(request=request, public=self.serve_public),
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/generators.py", line 285, in get_schema
    paths=self.parse(request, public),
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/generators.py", line 256, in parse
    operation = view.schema.get_operation(
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/openapi.py", line 112, in get_operation
    operation['responses'] = self._get_response_bodies()
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/openapi.py", line 1397, in _get_response_bodies
    return {'200': self._get_response_for_code(response_serializers, '200', direction=direction)}
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/openapi.py", line 1453, in _get_response_for_code
    component = self.resolve_serializer(serializer, direction)
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/openapi.py", line 1648, in resolve_serializer
    component.schema = self._map_serializer(serializer, direction, bypass_extensions)
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/openapi.py", line 949, in _map_serializer
    schema = self._map_basic_serializer(serializer, direction)
  File "/opt/netbox/venv/lib/python3.10/site-packages/drf_spectacular/openapi.py", line 1042, in _map_basic_serializer
    for field in serializer.fields.values():
  File "/usr/lib/python3.10/functools.py", line 981, in __get__
    val = self.func(instance)
  File "/home/vagrant/netbox-dev/netbox/netbox/netbox/api/serializers/base.py", line 64, in fields
    for key, value in self.get_fields().items():
  File "/opt/netbox/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 1105, in get_fields
    field_class, field_kwargs = self.build_field(
  File "/opt/netbox/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 1236, in build_field
    return self.build_standard_field(field_name, model_field)
  File "/opt/netbox/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 1260, in build_standard_field
    field_kwargs = get_field_kwargs(field_name, model_field)
  File "/opt/netbox/venv/lib/python3.10/site-packages/rest_framework/utils/field_mapping.py", line 242, in get_field_kwargs
    validator_kwarg += get_unique_validators(field_name, model_field)
  File "/home/vagrant/netbox-dev/netbox/netbox/netbox/monkey.py", line 31, in get_unique_validators
    conditions = {
  File "/home/vagrant/netbox-dev/netbox/netbox/netbox/monkey.py", line 33, in <setcomp>
    if cond.referenced_base_fields == field_set
AttributeError: 'NoneType' object has no attribute 'referenced_base_fields'

Where the regression was introduced

  • PR Fixes #19302: Fix uniqueness validation in REST API for nullable fields #20549 (“Fix uniqueness validation in REST API for nullable fields”) monkey‑patches DRF’s get_unique_validators() and pins DRF to 3.16.1.
  • The patch filters collected conditions with cond.referenced_base_fields == field_set. This fails when cond is None (unconditional constraint).
  • The patch is wired in settings.py via field_mapping.get_unique_validators = get_unique_validators.

Paths for reference:

  • netbox/netbox/monkey.py
  • netbox/netbox/settings.py

Why this matters

Unconditional, single‑field uniqueness enforced via UniqueConstraint is valid. The current filter assumes cond is always a Q‑object exposing referenced_base_fields, which isn’t true when the constraint is unconditional (None).

Proposed Fix (minimal & backward‑compatible)

Allow None through the filter while keeping the intended behavior (ignore conditions that reference other fields):

# Before
conditions = {
    cond for cond in conditions
    if cond.referenced_base_fields == field_set
}

# After (handle unconditional constraints)
conditions = {
    cond for cond in conditions
    if cond is None or getattr(cond, "referenced_base_fields", set()) == field_set
}

This preserves unconditional constraints (including unique=True) and continues to exclude conditions that reference fields other than the base field.

Additional Context

Happy to provide a PR for this. Thanks for taking a look at this follow‑up!

Metadata

Metadata

Assignees

Labels

severity: mediumResults in substantial degraded or broken functionality for specfic workflowsstatus: acceptedThis issue has been accepted for implementationtype: bugA confirmed report of unexpected behavior in the application

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions