-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Description
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.
- Run NetBox v4.4.3.
- Include a model that defines a single‑field
UniqueConstraintwith no condition (e.g., in a plugin model). - 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
conditionswithcond.referenced_base_fields == field_set. This fails whencond is None(unconditional constraint). - The patch is wired in
settings.pyviafield_mapping.get_unique_validators = get_unique_validators.
Paths for reference:
netbox/netbox/monkey.pynetbox/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
- I first hit this via my plugin, where a model defines a
UniqueConstraintwithout 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!