Consistent coalescing of Enums across APIs #11105

Closed
opened 2025-12-29 21:40:23 +01:00 by adam · 2 comments
Owner

Originally created by @corubba on GitHub (Apr 30, 2025).

Deployment Type

Self-hosted

NetBox Version

v4.2.8

Python Version

3.12

Steps to Reproduce

  1. Create a Site, Device Role, Manufacturer, Device Type, and Device; the way or their properties do not really matter
  2. Create a read+write API token
  3. Create a new Interface on the previously created device (ID 1 here) using the REST API. Type is required and set to a
    valid value, Duplex to an empty string, PoE-Mode to null, and PoE-Type is omitted
curl \
  --request 'POST' \
  --header 'Authorization: Token 1234...cdef' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data '{ "device": 1, "name": "MyInterface", "type": "other", "duplex": "", "poe_mode": null }' \
  'http://localhost:8000/api/dcim/interfaces/'
  1. Read the created Interface (ID 1 here) via the REST API
curl \
  --request 'GET' \
  --header 'Authorization: Token 1234...cdef' \
  --header 'Accept: application/json' \
  'http://localhost:8000/api/dcim/interfaces/1/'
  1. Read the created Interface (still ID 1 here) via the GraphQL API
curl \
  --request 'POST' \
  --header 'Authorization: Token 1234...cdef' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data '{ "query": "{ interface(id: 1) { id name type duplex poe_mode poe_type } }" }' \
  'http://localhost:8000/graphql/'

Expected Behavior

Reading the Interface via the REST API and the GraphQL API returns a consistent "empty" value for Duplex/PoE-Mode/PoE-Type, in this case null for all three.

Observed Behavior

In the database (PostgreSQL), Duplex and PoE-Mode are an empty string while PoE-Type is a true NULL.

netbox=# SELECT id, name, type, duplex, poe_mode, poe_type from dcim_interface;
 id |     name    | type  | duplex | poe_mode | poe_type
----+-------------+-------+--------+----------+----------
  1 | MyInterface | other |        |          |  [NULL]
(1 row)

In Step 4, the REST API coalesces Duplex and PoE-Mode, returning null for all three.

{
  "id": 1,
  "name": "MyInterface",
  "type": { "value": "other", "label": "Other" },
  "duplex": null,
  "poe_mode": null,
  "poe_type": null,
  [...]
}

In Step 5, the GraphQL API returns Duplex/PoE-Mode/PoE-Type as they are in the database.

{
  "data": {
    "interface": {
      "id": "1",
      "name": "MyInterface",
      "type": "other",
      "duplex": "",
      "poe_mode": "",
      "poe_type": null
    }
  },
  [...]
}

I am willing to work on this, given a rough solution direction and maybe some pointers what needs to be changed.

Type, Duplex, PoE-Mode and PoE-Type are enums/choices, which I think is relevant here because normal string fields like description appear to behave differently. Fixing this only at creation-time, so that a proper NULL is stored in the database, would require a migration of the existing "wrong" data. Fixing it only at read-time would not require a migration, but fix the symptom rather then the cause. Doing both would not require a data migration and the data would fix itself over time, but potentially means maintaining more code.

At the end of the day I (as a user) am not really interested in how the data is stored in the database, but that the APIs behave consistently with regards to omitted/empty values, both when reading and writing.

Originally created by @corubba on GitHub (Apr 30, 2025). ### Deployment Type Self-hosted ### NetBox Version v4.2.8 ### Python Version 3.12 ### Steps to Reproduce 1. Create a Site, Device Role, Manufacturer, Device Type, and Device; the way or their properties do not really matter 2. Create a read+write API token 3. Create a new Interface on the previously created device (ID 1 here) using the REST API. Type is required and set to a valid value, Duplex to an empty string, PoE-Mode to `null`, and PoE-Type is omitted ``` curl \ --request 'POST' \ --header 'Authorization: Token 1234...cdef' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data '{ "device": 1, "name": "MyInterface", "type": "other", "duplex": "", "poe_mode": null }' \ 'http://localhost:8000/api/dcim/interfaces/' ``` 4. Read the created Interface (ID 1 here) via the REST API ``` curl \ --request 'GET' \ --header 'Authorization: Token 1234...cdef' \ --header 'Accept: application/json' \ 'http://localhost:8000/api/dcim/interfaces/1/' ``` 5. Read the created Interface (still ID 1 here) via the GraphQL API ``` curl \ --request 'POST' \ --header 'Authorization: Token 1234...cdef' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data '{ "query": "{ interface(id: 1) { id name type duplex poe_mode poe_type } }" }' \ 'http://localhost:8000/graphql/' ``` ### Expected Behavior Reading the Interface via the REST API and the GraphQL API returns a consistent "empty" value for Duplex/PoE-Mode/PoE-Type, in this case `null` for all three. ### Observed Behavior In the database (PostgreSQL), Duplex and PoE-Mode are an empty string while PoE-Type is a true `NULL`. ``` netbox=# SELECT id, name, type, duplex, poe_mode, poe_type from dcim_interface; id | name | type | duplex | poe_mode | poe_type ----+-------------+-------+--------+----------+---------- 1 | MyInterface | other | | | [NULL] (1 row) ``` In Step 4, the REST API coalesces Duplex and PoE-Mode, returning `null` for all three. ``` { "id": 1, "name": "MyInterface", "type": { "value": "other", "label": "Other" }, "duplex": null, "poe_mode": null, "poe_type": null, [...] } ``` In Step 5, the GraphQL API returns Duplex/PoE-Mode/PoE-Type as they are in the database. ``` { "data": { "interface": { "id": "1", "name": "MyInterface", "type": "other", "duplex": "", "poe_mode": "", "poe_type": null } }, [...] } ``` <hr> I am willing to work on this, given a rough solution direction and maybe some pointers what needs to be changed. Type, Duplex, PoE-Mode and PoE-Type are enums/choices, which I think is relevant here because normal string fields like `description` appear to behave differently. Fixing this only at creation-time, so that a proper `NULL` is stored in the database, would require a migration of the existing "wrong" data. Fixing it only at read-time would not require a migration, but fix the symptom rather then the cause. Doing both would not require a data migration and the data would fix itself over time, but potentially means maintaining more code. At the end of the day I (as a user) am not really interested in how the data is stored in the database, but that the APIs behave consistently with regards to omitted/empty values, both when reading and writing.
adam added the type: bug label 2025-12-29 21:40:23 +01:00
adam closed this issue 2025-12-29 21:40:23 +01:00
Author
Owner

@arthanson commented on GitHub (May 1, 2025):

GraphQL and the REST API are completely different and are not expected to be consistent, the underlying libraries are completely different and there are no expectations of consistency between them.

Note: Please check NetBox 4.3 as GraphQL was changed quite a bit so this behavior may be different.

@arthanson commented on GitHub (May 1, 2025): GraphQL and the REST API are completely different and are not expected to be consistent, the underlying libraries are completely different and there are no expectations of consistency between them. Note: Please check NetBox 4.3 as GraphQL was changed quite a bit so this behavior may be different.
Author
Owner

@corubba commented on GitHub (May 2, 2025):

Okay, reasonable and fine by me.

Let me try a different angle then: When creating an object, what is the purpose of the storing-difference between omitting an enum-field (PoE-Type in the example) and explicitly setting null (Poe-Mode)? If there is one, why are these empty string fields (Duplex and PoE-Mode) always changed to null when editing+saving the object (e.g. the Interface from Step 3) via the WebUI, which is also shown in the diff of the ChangeLog entry? Applying the "completely different and are not expected to be consistent" argument between WebUI and REST would be kind of ridiculous. Maybe I am wrong, but this "sometimes use null, sometimes use empty string" feels like one of those systemic quirks that come back to bite you sooner or later. GraphQL was just where it showed since it apparently uses the database-stored values with less (or no) transformation.

This behaviour is still the same in 4.3.0, by the way.

@corubba commented on GitHub (May 2, 2025): Okay, reasonable and fine by me. Let me try a different angle then: When creating an object, what is the purpose of the storing-difference between omitting an enum-field (PoE-Type in the example) and explicitly setting `null` (Poe-Mode)? If there is one, why are these empty string fields (Duplex and PoE-Mode) always changed to `null` when editing+saving the object (e.g. the Interface from Step 3) via the WebUI, which is also shown in the diff of the ChangeLog entry? Applying the "completely different and are not expected to be consistent" argument between WebUI and REST would be kind of ridiculous. Maybe I am wrong, but this "sometimes use null, sometimes use empty string" feels like one of those systemic quirks that come back to bite you sooner or later. GraphQL was just where it showed since it apparently uses the database-stored values with less (or no) transformation. This behaviour is still the same in 4.3.0, by the way.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#11105