[BUG] transaction.non_atomic_requests is ignored on Ninja routes
Author: OscarVanLCreated Aug 28, 2026Updated Aug 28, 2026
Describe the bug
On my Django database configuration I have "ATOMIC_REQUESTS": True so that all HTTP requests automatically run in a transaction. This is to ensure atomicity in the case of request failures.
On some API endpoints, like my/health route, I want to exempt this from atomic requests since there are no DB queries in this route. This is normally achieved with @transaction.non_atomic_requests. However, this does not work on a Ninja route.
I think Ninja should honour this setting to allow for more granular transaction control.
Versions (please complete the following information):
- Python version: 3.12.14
- Django version: 5.2.17
- Django-Ninja version: 1.6.2
- Pydantic version: 2.13.0
Reproducible example
from django.conf import settings
settings.configure(
SECRET_KEY="repro",
ROOT_URLCONF=__name__,
ALLOWED_HOSTS=["testserver"],
INSTALLED_APPS=[],
MIDDLEWARE=[],
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
"ATOMIC_REQUESTS": True,
}
},
)
import django
django.setup()
from django.db import connection, transaction
from django.http import JsonResponse
from django.test import Client
from django.urls import path, resolve
from ninja import NinjaAPI
from ninja import __version__ as ninja_version
from ninja.decorators import decorate_view
# Control: Django's decorator disables ATOMIC_REQUESTS on a regular view
@transaction.non_atomic_requests
def django_view(request):
return JsonResponse({"in_atomic_block": connection.in_atomic_block})
api = NinjaAPI()
# Ninja: the same decorator has no effect on a ninja view
@api.get("/ninja-direct")
@transaction.non_atomic_requests
def direct_ninja_view(request):
return {"in_atomic_block": connection.in_atomic_block}
decorated_api = NinjaAPI(urls_namespace="decorated-api")
# Test the suggested workaround (not working)
try:
@decorated_api.get("/ninja-decorate-view")
@decorate_view(transaction.non_atomic_requests)
def decorated_ninja_view(request):
return {"in_atomic_block": connection.in_atomic_block}
except AttributeError as exc:
decorate_view_error = exc
else:
decorate_view_error = None
urlpatterns = [
path("django", django_view),
path("", api.urls),
]
if __name__ == "__main__":
client = Client()
django_result = client.get("/django").json()
direct_ninja_result = client.get("/ninja-direct").json()
direct_ninja_callback = resolve("/ninja-direct").func
print(f"Django {django.get_version()}")
print(f"django-ninja {ninja_version}")
print(f"Django view: {django_result}")
print(f"Direct Ninja view: {direct_ninja_result}")
print(f"Direct decorated function attribute: {direct_ninja_view._non_atomic_requests}")
print(f"Direct resolved callback attribute: {getattr(direct_ninja_callback, '_non_atomic_requests', None)}")
print(f"decorate_view registration error: {decorate_view_error!r}")
# The plain Django control is outside a transaction, while the equivalent Ninja view is still inside one.
assert django_result == {"in_atomic_block": False}
assert direct_ninja_result == {"in_atomic_block": True}
# decorate_view currently applies the decorator to a bound method, which cannot store Django's marker.
assert isinstance(decorate_view_error, AttributeError)
assert "'method' object has no attribute '_non_atomic_requests'" in str(decorate_view_error)Here's the output:
Django 5.2.17
django-ninja 1.6.2
Django view: {'in_atomic_block': False}
Direct Ninja view: {'in_atomic_block': True}
Direct decorated function attribute: {'default'}
Direct resolved callback attribute: None
decorate_view registration error: AttributeError("'method' object has no attribute '_non_atomic_requests'")Source: vitalik/django-ninja