GraphQL for Django, powered by graphql-core
and Pydantic. Define your GraphQL API straight from
your Django models — no DRF, no graphene, no django-filter.
- Model-first types & mutations —
DjangoModelType/DjangoModelMutationgive you query, list and create/update/delete from a singleMeta.model, validated and persisted with Pydantic v2 + the Django ORM (FK existence, uniqueness,unique_together, partial updates,choices→ Enum). - Logical filtering — one nested
filter:argument withand/or/not, per-field lookups, relation descent and plain-pk/UUID filtering (nodjango-filter). - Pagination — limit/offset, page and keyset cursor paginators with a
uniform
results/totalCountshape (and an automatic N+1 query optimizer). - Custom validation — DRF-style inline
validate_<field>()/validate()or aMeta.pydantic_model. - Permissions, security & directives — permission classes, depth & cost limits,
introspection control, and string/number/date/list directives.
Meta.only_fields/Meta.exclude_fieldsare a security boundary: a column a type hides is unreadable, unorderable and unfilterable through it. - Subscriptions — real-time GraphQL over Django Channels 4 (optional extra).
Coming from
graphene-djangoorgraphene-django-extras? See the Migration Guide for a step-by-step upgrade with before/after examples.Upgrading from
django-graphex1.x? 2.0 removed the graphene backend entirely — see the Upgrade Guide and thescripts/migrate_2_0.pycodemod.
- Python: 3.12+ (3.13, 3.14 supported)
- Django: 5.2+ (5.2 LTS, 6.0 supported) — each Django version tested on the Python versions it officially supports
- graphql-core: >=3.3.0,<3.4
- pydantic: >=2,<3
django-graphex 4.0.0 requires graphql-core>=3.3.0,<3.4. The historical
3.1.1 release requires graphql-core>=3.2.13,<3.3. Follow the
4.0 upgrade guide when migrating custom executors,
AST builders or subscription transports.
# uv (recommended)
uv add "django-graphex==4.0.0"
# real-time subscriptions (adds Django Channels 4):
uv add "django-graphex[subscriptions]==4.0.0"# pip
pip install "django-graphex==4.0.0"
pip install "django-graphex[subscriptions]==4.0.0"The base install never imports channels; only the subscriptions extra does.
from django.contrib.auth import get_user_model
from django.urls import path
from django_graphex.core import BooleanField, CharField, Field, Mutation, ObjectType
from django_graphex.fields import DjangoListObjectField, DjangoObjectField
from django_graphex.paginations import LimitOffsetGraphqlPagination
from django_graphex.schema import DjangoGraphQLSchema
from django_graphex.types import DjangoListObjectType, DjangoObjectType
from django_graphex.views import AuthenticatedGraphQLView
User = get_user_model()
class UserType(DjangoObjectType):
class Meta:
model = User
only_fields = ("id", "username", "first_name", "last_name")
filter_fields = {"username": ("icontains", "exact")}
class UserListType(DjangoListObjectType):
class Meta:
model = User
pagination = LimitOffsetGraphqlPagination()
class RegisterUser(Mutation):
ok = BooleanField()
user = Field(UserType)
class Arguments:
username = CharField(required=True)
password = CharField(required=True)
@classmethod
def mutate(cls, root, info, username, password):
user = User.objects.create_user(username=username, password=password)
return cls(ok=True, user=user)
class Query(ObjectType):
user = DjangoObjectField(UserType)
users = DjangoListObjectField(UserListType)
class Mutation(ObjectType):
register_user = RegisterUser.Field()
schema = DjangoGraphQLSchema(query=Query, mutation=Mutation)
urlpatterns = [
path("graphql/", AuthenticatedGraphQLView.as_view(schema=schema, graphiql=True)),
]User is deliberately not mounted through a generic model mutation:
account creation must call create_user() so the password is hashed, and the
client must never choose staff/superuser flags. Use generated mutations for
ordinary application models; see the mutation guide.
Query it with the nested filter: argument (and / or / not):
{
users(filter: { isActive: { exact: true }, username: { icontains: "jo" } }) {
results(limit: 10, ordering: "-date_joined") { id username }
totalCount
}
}All settings live under a single DJANGO_GRAPHEX dict (every key is optional):
# settings.py
DJANGO_GRAPHEX = {
"DEFAULT_PAGINATION_CLASS": "django_graphex.paginations.LimitOffsetGraphqlPagination",
"DEFAULT_PAGE_SIZE": 20,
"MAX_PAGE_SIZE": 50,
# Response caching. Default is False (disabled).
# WARNING: cache keys are identity-salted per user (v1.2.1+), but shared
# caches can still leak data if misconfigured. Review the caching guide
# before enabling in production: docs/usage/caching.md
"CACHE_ACTIVE": False,
}Two keys are not in that dict on purpose — both ship enabled, and pinning a key to its own default only hides that you depend on it:
REQUIRE_CSRF_HEADER(True) demands anX-Requested-Withheader on form-encoded andmultipart/form-dataPOSTs, which a browser can send cross-site with no CORS preflight.application/jsonclients are unaffected; form-encoded and multipart clients get HTTP 403 without it.MAX_SUBSCRIPTIONS_PER_CONNECTION(50) caps the concurrent operations onegraphql-transport-wssocket may hold.
Both are documented in Settings, and both are breaking for clients written against 2.2.0.
To use directives, add the middleware and pass all_directives to the schema:
DJANGO_GRAPHEX = {"MIDDLEWARE": ["django_graphex.middleware.GraphQLDirectiveMiddleware"]}
from django_graphex.directives import all_directives
from django_graphex.schema import DjangoGraphQLSchema
schema = DjangoGraphQLSchema(query=Query, mutation=Mutation, directives=all_directives)A fully wired example project lives in examples/playground/. It exercises every major feature end-to-end — types, paginators, filtering, mutations, permissions, subscriptions, file uploads on both paths, the projection boundary on all three axes, and the query optimizer — and installs the library from this repo checkout (editable, no PyPI release needed). make test runs the end-to-end suite; several tests assert the verbatim answer strings its README quotes.
📚 Full documentation — including the Quick Start, Model backend, Filtering, Pagination, Subscriptions, Settings and the Migration Guide.
The current core33 comparison is
derived from eight committed portable results in core33-4.0.0-bf6e1ed6ccb6d3940d253c9c83b19a436f01aa4e,
measured from the prepared 4.0.0 source bf6e1ed6ccb6d3940d253c9c83b19a436f01aa4e. In the SQLite
create-comment operation, GraphEx/Ariadne p50 was 0.57× at 50,000 comments
and 0.51× at 100,000; these are whole-stack observations under accepted
background load, not an equal-core or universal performance guarantee.
Earlier 4.0.0 pre-fragment, 3.1.1-source core33 and 3.1.0 figures remain historical.
MIT License — see the LICENSE file.