Skip to content

Fix OpenAPI schema generation#628

Open
pederhan wants to merge 9 commits into
masterfrom
fix-schema
Open

Fix OpenAPI schema generation#628
pederhan wants to merge 9 commits into
masterfrom
fix-schema

Conversation

@pederhan

@pederhan pederhan commented Apr 8, 2026

Copy link
Copy Markdown
Member

This PR fixes OpenAPI schema generation by switching from the built-in DRF OpenAPI schema generator to drf-spectacular, as outlined in #627.

By using drf-spectacular, we get the following endpoints:

  • /docs: Swagger UI for viewing the schema (default).
  • /docs/redoc: ReDoc UI for viewing the schema (alternative).
  • /docs/schema: The OpenAPI schema (YAML file) (/docs kept for backwards compatibility).

Since we can't assume MREG can access the internet to fetch static files for serving swagger/redoc UI pages, we include them manually via the drf-spectacular[sidecar] extra. Because both Swagger and ReDoc static files come from the same package, including endpoints for both costs nothing extra in terms of final container image size.

Issues

Using drf-spectacular highlights numerous issues with our views:

❯ uv run ./manage.py spectacular --color --file schema.yml
mreg/api/views.py:264: Error [HealthHeartbeat]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/views.py:274: Error [HealthLDAP]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/views.py:243: Error [MetaVersions]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/views.py:315: Error [MetricsView]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/views.py:161: Error [UserInfo]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/views.py:233: Error [MregVersion]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/views.py:150: Error [TokenIsValid]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/views.py:140: Error [TokenLogout]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/serializers.py:252: Warning [BACnetIDList > BACnetIDSerializer]: unable to resolve type hint for function "hostname". Consider using a type hint or @extend_schema_field. Defaulting to string.
mreg/api/v1/serializers.py:252: Warning [BACnetIDDetail > BACnetIDSerializer]: unable to resolve type hint for function "hostname". Consider using a type hint or @extend_schema_field. Defaulting to string.
mreg/api/v1/views.py: Error [dhcp_hosts_by_range]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Warning [dhcp_hosts_by_range]: could not derive type of path parameter "ip" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:ip>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views.py: Warning [dhcp_hosts_by_range]: could not derive type of path parameter "range" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:range>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views.py: Error [dhcp_hosts_all_v4]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [dhcp_hosts_all_v6]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py:1228: Error [DhcpHostsV4ByV6]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py:1228: Warning [DhcpHostsV4ByV6]: could not derive type of path parameter "ip" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:ip>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views.py:1228: Warning [DhcpHostsV4ByV6]: could not derive type of path parameter "range" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:range>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:119: Warning [HostGroupGroupsList]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:119: Warning [HostGroupGroupsList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: 'name')
mreg/api/v1/views_hostgroups.py:133: Warning [HostGroupGroupsDetail]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:133: Warning [HostGroupGroupsDetail]: could not derive type of path parameter "group" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:group>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:150: Warning [HostGroupHostsList]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:150: Warning [HostGroupHostsList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: 'name')
mreg/api/v1/views_hostgroups.py:164: Warning [HostGroupHostsDetail]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:164: Warning [HostGroupHostsDetail]: could not derive type of path parameter "host" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:host>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:181: Warning [HostGroupOwnersList]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:181: Warning [HostGroupOwnersList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: 'name')
mreg/api/v1/views_hostgroups.py:196: Warning [HostGroupOwnersDetail]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_hostgroups.py:196: Warning [HostGroupOwnersDetail]: could not derive type of path parameter "owner" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:owner>) or annotating the parameter type with @extend_schema. Defaulting to "string".
hostpolicy/api/v1/views.py:174: Warning [HostPolicyRoleAtomsList]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
hostpolicy/api/v1/views.py:174: Warning [HostPolicyRoleAtomsList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: 'name')
hostpolicy/api/v1/views.py:188: Warning [HostPolicyRoleAtomsDetail]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
hostpolicy/api/v1/views.py:188: Warning [HostPolicyRoleAtomsDetail]: could not derive type of path parameter "atom" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:atom>) or annotating the parameter type with @extend_schema. Defaulting to "string".
hostpolicy/api/v1/views.py:205: Warning [HostPolicyRoleHostsList]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
hostpolicy/api/v1/views.py:205: Warning [HostPolicyRoleHostsList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: 'name')
hostpolicy/api/v1/views.py:219: Warning [HostPolicyRoleHostsDetail]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
hostpolicy/api/v1/views.py:219: Warning [HostPolicyRoleHostsDetail]: could not derive type of path parameter "host" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:host>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/serializers.py:33: Warning [HostList > HostSerializer > HostCommunityMappingSerializer > CommunitySerializer]: unable to resolve type hint for function "get_hosts". Consider using a type hint or @extend_schema_field. Defaulting to string.
mreg/api/v1/serializers.py:33: Warning [HostList > HostSerializer > HostCommunityMappingSerializer > CommunitySerializer]: unable to resolve type hint for function "get_global_name". Consider using a type hint or @extend_schema_field. Defaulting to string.
mreg/api/v1/views.py:494: Error [HostContactsView]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py:494: Warning [HostContactsView]: could not derive type of path parameter "name" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:name>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/serializers.py:819: Error [NetworkList > NetworkSerializer]: could not resolve model field "mreg.Network.network". Failed to resolve through serializer_field_mapping, get_internal_type(), or any override mechanism. Defaulting to "string"
mreg/api/v1/serializers.py:819: Error [NetworkDetail > NetworkSerializer]: could not resolve model field "mreg.Network.network". Failed to resolve through serializer_field_mapping, get_internal_type(), or any override mechanism. Defaulting to "string"
mreg/api/v1/serializers.py:33: Warning [NetworkCommunityDetail > CommunitySerializer]: unable to resolve type hint for function "get_hosts". Consider using a type hint or @extend_schema_field. Defaulting to string.
mreg/api/v1/serializers.py:33: Warning [NetworkCommunityDetail > CommunitySerializer]: unable to resolve type hint for function "get_global_name". Consider using a type hint or @extend_schema_field. Defaulting to string.
mreg/api/v1/views_network_policy.py:218: Warning [NetworkCommunityHostList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: Network not found.)
mreg/api/v1/views.py:961: Warning [NetworkExcludedRangeList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: 'network')
mreg/api/v1/views.py: Error [network_first_unused]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_ptroverride_host_list]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_ptroverride_list]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_random_unused]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_reserved_list]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_unused_count]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_unused_list]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_used_count]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_used_host_list]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_used_list]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Error [network_by_ip]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views.py: Warning [network_by_ip]: could not derive type of path parameter "ip" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:ip>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/serializers.py:582: Error [NetGroupRegexPermissionList > NetGroupRegexPermissionSerializer]: could not resolve model field "mreg.NetGroupRegexPermission.range". Failed to resolve through serializer_field_mapping, get_internal_type(), or any override mechanism. Defaulting to "string"
mreg/api/v1/serializers.py:582: Error [NetGroupRegexPermissionDetail > NetGroupRegexPermissionSerializer]: could not resolve model field "mreg.NetGroupRegexPermission.range". Failed to resolve through serializer_field_mapping, get_internal_type(), or any override mechanism. Defaulting to "string"
mreg/api/v1/views_zones.py: Error [zone_file_detail]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views_zones.py:153: Warning [ForwardZoneDelegationList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: 'name')
mreg/api/v1/views_zones.py: Error [forward_zone_by_hostname]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
mreg/api/v1/views_zones.py: Warning [forward_zone_by_hostname]: could not derive type of path parameter "hostname" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. <int:hostname>) or annotating the parameter type with @extend_schema. Defaulting to "string".
mreg/api/v1/views_zones.py:159: Warning [ReverseZoneDelegationList]: Failed to obtain model through view's queryset due to raised exception. Prevent this either by setting "queryset = Model.objects.none()" on the view, checking for "getattr(self, "swagger_fake_view", False)" in get_queryset() or by simply using @extend_schema. (Exception: 'name')
Warning: operationId "api_v1_dhcphosts_ipv6byipv4_retrieve" has collisions [('/api/v1/dhcphosts/ipv6byipv4/', 'get'), ('/api/v1/dhcphosts/ipv6byipv4/{ip}/{range}', 'get')]. resolving with numeral suffixes.

Schema generation summary:
Warnings: 75 (38 unique)
Errors:   119 (30 unique)

@coveralls

coveralls commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator

Coverage Status

coverage: 97.697% (-0.07%) from 97.767% — fix-schema into master

@pederhan pederhan linked an issue Apr 13, 2026 that may be closed by this pull request
@terjekv

terjekv commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Standardised error responses to always use lower case via an error_response helper. Previously we had both ERROR and error used as keys. This standardisation also helps schema generation. Also note that we have an error_body for those situations where we're pushing the error up as an exception. Not sure this is a long-term pattern we should retain, but it's there for consistency for now.

Also added more specific serializers to outputs to ensure we have "typed" output that def-spectacular can work with.

Overall, this makes all endpoints acceptable to drf-spectacular. We also try to provide some meta information to text endpoints via @extend_schema.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Schema endpoint (/docs/) broken

3 participants