diff --git a/pyproject.toml b/pyproject.toml
index 69b8e3c810..a17e5c57af 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,6 +36,7 @@ dependencies = [
"Django>=4.2,<4.3",
"django-filter>=2",
"djangorestframework>=3.12",
+ "drf-spectacular",
"pytz",
"iso8601",
diff --git a/python/nav/django/settings.py b/python/nav/django/settings.py
index 75fa799054..cd7c85a0c8 100644
--- a/python/nav/django/settings.py
+++ b/python/nav/django/settings.py
@@ -221,6 +221,7 @@
'django.contrib.humanize',
'django_filters',
'rest_framework',
+ 'drf_spectacular',
'nav.auditlog',
'nav.web.macwatch',
'nav.web.geomap',
@@ -235,6 +236,12 @@
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
'DEFAULT_PAGINATION_CLASS': 'nav.web.api.v1.NavPageNumberPagination',
'UNAUTHENTICATED_USER': 'nav.django.utils.default_account',
+ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
+}
+
+SPECTACULAR_SETTINGS = {
+ 'TITLE': 'NAV API',
+ 'PREPROCESSING_HOOKS': ['nav.web.api.schema.public_schema_filter'],
}
# Classes that implement a search engine for the web navbar
diff --git a/python/nav/web/api/schema.py b/python/nav/web/api/schema.py
new file mode 100644
index 0000000000..2cc8192d41
--- /dev/null
+++ b/python/nav/web/api/schema.py
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2025 Sikt
+#
+# This file is part of Network Administration Visualized (NAV).
+#
+# NAV is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3 as published by
+# the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details. You should have received a copy of the GNU General Public
+# License along with NAV. If not, see .
+#
+import re
+
+PUBLIC_SCHEMA_FILTER = re.compile(r'^/api/\d+/.*$')
+
+
+def public_schema_filter(endpoints):
+ """drf-spectacular preprocessing filter to filter out DRF endpoints we don't want to 'expose' as public API.
+
+ This is mostly because of NAV's weird DRF usage, where the latest API version is bound both to the /api/ and
+ /api// URL prefixes. Some NAV apps also provide internal APIs that should not be exposed as part of the
+ public schema. This function basically ensures only endpoints with the /api// prefix are included in
+ the schema.
+ """
+ for endpoint in endpoints:
+ path, _path_regex, _method, _callback = endpoint
+ if PUBLIC_SCHEMA_FILTER.match(path):
+ yield endpoint
diff --git a/python/nav/web/api/v1/urls.py b/python/nav/web/api/v1/urls.py
index 04a1fe6e31..916c4cb084 100644
--- a/python/nav/web/api/v1/urls.py
+++ b/python/nav/web/api/v1/urls.py
@@ -17,7 +17,8 @@
# pylint: disable=E1101
"""Urlconf for the NAV REST api"""
-from django.urls import re_path, include
+from django.urls import path, re_path, include
+from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from rest_framework import routers
from nav.auditlog import api as auditlogapi
@@ -54,8 +55,17 @@
router.register(r'auditlog', auditlogapi.LogEntryViewSet, basename='auditlog')
router.register(r'module', views.ModuleViewSet, basename='module')
+openapi_urls = [
+ path('', SpectacularAPIView.as_view(api_version="1"), name='schema'),
+ path(
+ "swagger-ui/",
+ SpectacularSwaggerView.as_view(url_name="api:1:openapi:schema"),
+ name="swagger-ui",
+ ),
+]
urlpatterns = [
+ path("schema/", include((openapi_urls, "openapi"))),
re_path(r'^$', views.api_root),
re_path(r'^token/$', views.get_or_create_token, name="token"),
re_path(r'^version/$', views.get_nav_version, name="version"),
diff --git a/requirements/base.txt b/requirements/base.txt
index 37fdd902fa..b00564c04a 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -24,6 +24,7 @@ dnspython<3.0.0,>=2.1.0
django-filter>=2
djangorestframework>=3.12
+drf-spectacular
# REST framework
iso8601