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

Closed
opened 2025-12-29 21:49:12 +01:00 by adam · 1 comment
Owner

Originally created by @pheus on GitHub (Oct 15, 2025).

Originally assigned to: @jeremystretch on GitHub.

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 #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!

Originally created by @pheus on GitHub (Oct 15, 2025). Originally assigned to: @jeremystretch on GitHub. ### 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) ```python 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()`: ````text AttributeError: 'NoneType' object has no attribute 'referenced_base_fields' ```` #### Full traceback (from my environment) ````text 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 **#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): ````python # 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 - I first hit this via my plugin, where a model defines a `UniqueConstraint` without an explicit condition: <https://github.com/pheus/netbox-aci-plugin/blob/main/netbox_aci_plugin/models/tenant/tenants.py> Happy to provide a PR for this. Thanks for taking a look at this follow‑up!
adam added the type: bugstatus: acceptedseverity: medium labels 2025-12-29 21:49:12 +01:00
adam closed this issue 2025-12-29 21:49:12 +01:00
Author
Owner

@jeremystretch commented on GitHub (Oct 15, 2025):

Thanks for such a thorough report @pheus! Curious that it never presented an issue in core, but it seems we've always taken to setting unique=True on individual model fields instead of using UniqueConstraint. You're correct though:

Unconditional, single‑field uniqueness enforced via UniqueConstraint is valid.

Apologies for the oversight!

@jeremystretch commented on GitHub (Oct 15, 2025): Thanks for such a thorough report @pheus! Curious that it never presented an issue in core, but it seems we've always taken to setting `unique=True` on individual model fields instead of using UniqueConstraint. You're correct though: > Unconditional, single‑field uniqueness enforced via `UniqueConstraint` is valid. Apologies for the oversight!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#11729