API blocks creation of child region with same name as top-level region, but UI allows it #11078

Closed
opened 2025-12-29 21:39:55 +01:00 by adam · 12 comments
Owner

Originally created by @mjoshionemind on GitHub (Apr 24, 2025).

Originally assigned to: @jeremystretch on GitHub.

Deployment Type

Self-hosted

NetBox Version

v4.2.3

Python Version

3.10

Steps to Reproduce

  1. Go to the NetBox UI and navigate to the Regions section.
    Create a new region:Name: Hong Kong, Slug: HK

Image

  1. After creation, create a child region from the UI with the following details:
    Name: Hong Kong,Slug: HK (or any valid unique slug),Parent: Hong Kong (the region created in step 1)

Image

  1. Observe that this is allowed and saved successfully.
  2. Now, attempt the same operation using the API:
    Make a POST request to /api/dcim/regions/ with this payload:
    {
    "name": "Hong Kong",
    "slug": "hk-kong",
    "parent": <id_of_Hong_Kong_region_created_in _step_1>
    }
    Image

Expected Behavior

The child region with the same name (Hong Kong) but a different slug should be created successfully under the parent Hong Kong, matching the behavior observed in the UI.

Observed Behavior

The API returns a 400 Bad Request with an error indicating that the name must be unique or conflicts with the parent, even though the UI allows it.

Originally created by @mjoshionemind on GitHub (Apr 24, 2025). Originally assigned to: @jeremystretch on GitHub. ### Deployment Type Self-hosted ### NetBox Version v4.2.3 ### Python Version 3.10 ### Steps to Reproduce 1. Go to the NetBox UI and navigate to the Regions section. Create a new region:Name: Hong Kong, Slug: HK ![Image](https://github.com/user-attachments/assets/a054644b-9e22-4266-8944-16c9f7b5ed18) 2. After creation, create a child region from the UI with the following details: Name: Hong Kong,Slug: HK (or any valid unique slug),Parent: Hong Kong (the region created in step 1) ![Image](https://github.com/user-attachments/assets/1c9ec8aa-1862-4ebd-9603-93ac957f8ef5) 3. Observe that this is allowed and saved successfully. 4. Now, attempt the same operation using the API: Make a POST request to /api/dcim/regions/ with this payload: { "name": "Hong Kong", "slug": "hk-kong", "parent": <id_of_Hong_Kong_region_created_in _step_1> } ![Image](https://github.com/user-attachments/assets/2f73311e-c9fb-47a9-83a6-cce50000da63) ### Expected Behavior The child region with the same name (Hong Kong) but a different slug should be created successfully under the parent Hong Kong, matching the behavior observed in the UI. ### Observed Behavior The API returns a 400 Bad Request with an error indicating that the name must be unique or conflicts with the parent, even though the UI allows it.
adam added the type: bugstatus: acceptedseverity: low labels 2025-12-29 21:39:55 +01:00
adam closed this issue 2025-12-29 21:39:56 +01:00
Author
Owner

@jeremystretch commented on GitHub (Apr 24, 2025):

@yjain18111 @rraj9921 @yash-pal1 I notice you each gave this a 👍 immediately after it was opened. Would one of you like to own it?

@jeremystretch commented on GitHub (Apr 24, 2025): @yjain18111 @rraj9921 @yash-pal1 I notice you each gave this a 👍 immediately after it was opened. Would one of you like to own it?
Author
Owner

@arthanson commented on GitHub (Apr 25, 2025):

Tried this on NetBox v4.2.8 and it is working fine now, looks like it was fixed, also the error message that was referenced is no longer even in the code. Closing.

@arthanson commented on GitHub (Apr 25, 2025): Tried this on NetBox v4.2.8 and it is working fine now, looks like it was fixed, also the error message that was referenced is no longer even in the code. Closing.
Author
Owner

@abhi1693 commented on GitHub (Apr 25, 2025):

@arthanson I check on v4.2.8 as well as develop branches and this issue exist in both. The error is most likely from DRF rather than netbox code. I suspect something with unique constraint is not working as expected. In any case, this issue must be reopened as the last working version for this issue is v3.7.8.

Image

Image

@abhi1693 commented on GitHub (Apr 25, 2025): @arthanson I check on v4.2.8 as well as develop branches and this issue exist in both. The error is most likely from DRF rather than netbox code. I suspect something with unique constraint is not working as expected. In any case, this issue must be reopened as the last working version for this issue is v3.7.8. ![Image](https://github.com/user-attachments/assets/f6f4a094-c726-4ef9-93bf-0b690eb292de) ![Image](https://github.com/user-attachments/assets/369deb00-aab2-4f6f-9bbb-ba3e6777697f)
Author
Owner

@abhi1693 commented on GitHub (Apr 25, 2025):

Here's a minimal code to reproduce this issue

from dcim.api.serializers import RegionSerializer
serializer=RegionSerializer(data={"name": "Hong Kong","slug": "hk-kong","parent": 21})
serializer.is_valid() # This returns False
serializer.errors # {'name': [ErrorDetail(string='region with this name already exists.', code='unique')]}
@abhi1693 commented on GitHub (Apr 25, 2025): Here's a minimal code to reproduce this issue ``` from dcim.api.serializers import RegionSerializer serializer=RegionSerializer(data={"name": "Hong Kong","slug": "hk-kong","parent": 21}) serializer.is_valid() # This returns False serializer.errors # {'name': [ErrorDetail(string='region with this name already exists.', code='unique')]} ```
Author
Owner

@abhi1693 commented on GitHub (Apr 25, 2025):

I found the issue is being caused by https://github.com/netbox-community/netbox/blob/main/netbox/dcim/models/sites.py#L49. If you comment this constraint, the API works just as the UI.

@arthanson Can you re-confirm?

@abhi1693 commented on GitHub (Apr 25, 2025): I found the issue is being caused by https://github.com/netbox-community/netbox/blob/main/netbox/dcim/models/sites.py#L49. If you comment this constraint, the API works just as the UI. @arthanson Can you re-confirm?
Author
Owner

@arthanson commented on GitHub (Apr 25, 2025):

@abhi1693 that's weird, your example code works fine on mine and I had originally tried this via the REST API and it worked.

>>> from dcim.api.serializers import RegionSerializer
>>> serializer=RegionSerializer(data={"name": "Hong Kong","slug": "hk-kong","parent": 83})
>>> serializer.is_valid()
True
>>> obj = Region.objects.get(id=83)
>>> obj.name, obj.slug
('hong kong', 'HK')
@arthanson commented on GitHub (Apr 25, 2025): @abhi1693 that's weird, your example code works fine on mine and I had originally tried this via the REST API and it worked. ``` >>> from dcim.api.serializers import RegionSerializer >>> serializer=RegionSerializer(data={"name": "Hong Kong","slug": "hk-kong","parent": 83}) >>> serializer.is_valid() True >>> obj = Region.objects.get(id=83) >>> obj.name, obj.slug ('hong kong', 'HK') ```
Author
Owner

@mjoshionemind commented on GitHub (Apr 25, 2025):

@arthanson It looks like you're creating the child region as "Hong Kong" (capitalized), while the parent is "hong kong" (lowercase). There’s a difference between the two:

  1. Parent region: "Hong Kong"
    Child region: "hong kong"

Image
2. Parent region: "Hong Kong"
Child region: "Hong Kong"

Image

@mjoshionemind commented on GitHub (Apr 25, 2025): @arthanson It looks like you're creating the child region as "Hong Kong" (capitalized), while the parent is "hong kong" (lowercase). There’s a difference between the two: 1. Parent region: "Hong Kong" Child region: "hong kong" ![Image](https://github.com/user-attachments/assets/bf2580d0-ddc1-41dc-8398-70e3f0aebf06) 2. Parent region: "Hong Kong" Child region: "Hong Kong" ![Image](https://github.com/user-attachments/assets/d4227509-a66e-4718-8f73-14fea3272407)
Author
Owner

@github-actions[bot] commented on GitHub (Aug 6, 2025):

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. Do not attempt to circumvent this process by "bumping" the issue; doing so will result in its immediate closure and you may be barred from participating in any future discussions. Please see our contributing guide.

@github-actions[bot] commented on GitHub (Aug 6, 2025): This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. **Do not** attempt to circumvent this process by "bumping" the issue; doing so will result in its immediate closure and you may be barred from participating in any future discussions. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/main/CONTRIBUTING.md).
Author
Owner

@jeremystretch commented on GitHub (Sep 4, 2025):

I've confirmed that this still occurs on NetBox v4.4.0. I suspect this has something to do with DRF bug #9410, but I need to dig into it further.

The above-mentioned bug does not seem to be relevant.

@jeremystretch commented on GitHub (Sep 4, 2025): I've confirmed that this still occurs on NetBox v4.4.0. ~I suspect this has something to do with [DRF bug #9410](https://github.com/encode/django-rest-framework/issues/9410), but I need to dig into it further.~ The above-mentioned bug does not seem to be relevant.
Author
Owner

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

I think I've managed to track down the root issue. Here's an example REST API query to trigger the exception above, for reference. (Here, a top-level Region named "Foo" already exists.)

curl -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://netbox:8000/api/dcim/regions/ \
--data '{"parent": 4, "name": "Foo", "slug": "foo"}'

When RegionSerializer is initialized, a UniqueValidator instance is attached to its name and slug fields:

from dcim.api.serializers import RegionSerializer
s = RegionSerializer()
s.fields['name'].validators[0]
<UniqueValidator(queryset=<TreeQuerySet [<Region: Africa>, <Region: Asia>, <Region: Europe>, <Region: Foo>, ...]>)

The inclusion of this validator (on the name field) is prompted by this UniqueConstraint defined on the Region model:

models.UniqueConstraint(
    fields=('name',),
    name='%(app_label)s_%(class)s_name',
    condition=Q(parent__isnull=True),
    violation_error_message=_("A top-level region with this name already exists.")
),

The UniqueConstraint effectively says "Any region not assigned to a parent must have a unique name." However, the UniqueValidator created by DRF ignores the crucial context that the region we're attempting to create has a parent assigned: The name field is being validated absent context from the parent field.

This appears to be limitation in DRF's support for conditional UniqueConstraints, but I'll keep working on it.

@jeremystretch commented on GitHub (Oct 8, 2025): I think I've managed to track down the root issue. Here's an example REST API query to trigger the exception above, for reference. (Here, a top-level Region named "Foo" already exists.) ``` curl -X POST \ -H "Authorization: Token $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json; indent=4" \ http://netbox:8000/api/dcim/regions/ \ --data '{"parent": 4, "name": "Foo", "slug": "foo"}' ``` When RegionSerializer is initialized, a UniqueValidator instance is attached to its `name` and `slug` fields: ```python from dcim.api.serializers import RegionSerializer s = RegionSerializer() s.fields['name'].validators[0] <UniqueValidator(queryset=<TreeQuerySet [<Region: Africa>, <Region: Asia>, <Region: Europe>, <Region: Foo>, ...]>) ``` The inclusion of this validator (on the `name` field) is prompted by this UniqueConstraint defined on the Region model: ```python models.UniqueConstraint( fields=('name',), name='%(app_label)s_%(class)s_name', condition=Q(parent__isnull=True), violation_error_message=_("A top-level region with this name already exists.") ), ``` The UniqueConstraint effectively says "Any region not assigned to a parent must have a unique name." However, the [UniqueValidator](https://www.django-rest-framework.org/api-guide/validators/#uniquevalidator) created by DRF ignores the crucial context that the region we're attempting to create _has_ a parent assigned: The `name` field is being validated absent context from the `parent` field. This appears to be limitation in DRF's support for conditional UniqueConstraints, but I'll keep working on it.
Author
Owner

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

One way we could work around this is to collapse the two UniqueConstraints on the Region model into one, by adding nulls_distinct=False:

class Meta:
    constraints = (
        models.UniqueConstraint(
            fields=('parent', 'name'),
            name='%(app_label)s_%(class)s_parent_name',
            nulls_distinct=False,
        ),
    ...

DRF would then no longer attach a UniqueValidator to the name field, because the constraint applies to the combined set of parent and name.

Unfortunately, the NULLS NOT DISTINCT qualifier is not supported by PostgreSQL releases earlier than 15, as indicated by this warning in the resulting Django migration:

dcim.Region: (models.W047) PostgreSQL does not support unique constraints with nulls distinct.

We would need to raise the minimum supported version of PostgreSQL from 14 to 15 (or later) to support this approach.

@jeremystretch commented on GitHub (Oct 9, 2025): One way we could work around this is to collapse the two UniqueConstraints on the Region model into one, by adding `nulls_distinct=False`: ```python class Meta: constraints = ( models.UniqueConstraint( fields=('parent', 'name'), name='%(app_label)s_%(class)s_parent_name', nulls_distinct=False, ), ... ``` DRF would then no longer attach a UniqueValidator to the `name` field, because the constraint applies to the combined set of `parent` and `name`. Unfortunately, the `NULLS NOT DISTINCT` qualifier is not supported by PostgreSQL releases earlier than [15](https://www.postgresql.org/docs/release/15.0/), as indicated by this warning in the resulting Django migration: ``` dcim.Region: (models.W047) PostgreSQL does not support unique constraints with nulls distinct. ``` We would need to raise the minimum supported version of PostgreSQL from 14 to 15 (or later) to support this approach.
Author
Owner

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

As an interim workaround, we might need to monkey-patch DRF's get_unique_validators() function and ignore constraint conditions which reference other fields. That would keep the fix relatively contained until we're able to alter the constraints in a future release.

@jeremystretch commented on GitHub (Oct 9, 2025): As an interim workaround, we might need to monkey-patch DRF's [`get_unique_validators()`](https://github.com/encode/django-rest-framework/blob/a323cf7c0a33d7ffd395a6805019f613fb79f985/rest_framework/utils/field_mapping.py#L65) function and ignore constraint conditions which reference other fields. That would keep the fix relatively contained until we're able to alter the constraints in a future release.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#11078