Types¶
django-graphex provides enhanced type classes built on top of graphene's basic functionality.
Model choices → GraphQL enum
A model field with choices is converted to a GraphQL enum automatically.
Every declaration form is supported, including the Django 5.0 forms — an
enumeration type (TextChoices / IntegerChoices), a mapping, or a callable —
on all supported Django versions (the converter normalizes them). See
choices → GraphQL enums for how member names are
derived.
DjangoObjectType¶
The base node type: it maps a Django model to a GraphQL object type. Declare a
model and the library converts its fields (including choices → enums and
relations → nested list types). It is the building block every other type and
field resolves to.
from django_graphex import DjangoObjectType
from django.contrib.auth.models import User
class UserType(DjangoObjectType):
class Meta:
description = "Type definition for a single user"
model = User
# Optional: restrict / shape the exposed fields.
only_fields = ("id", "username", "email", "is_active")
# Enables `filter:` when this type is used as a (nested) list.
filter_fields = {"username": ("exact", "icontains")}
Use it directly with DjangoObjectField to fetch a
single object by id, as the node of a DjangoListObjectType,
or as the output of a mutation. Relations on the model are exposed
as nested lists with the uniform results / totalCount shape — see
Nested lists.
Per-request queryset
Override get_queryset(cls, queryset, info) to scope what a type returns
(e.g. only the current user's rows). See
Custom queryset.
DjangoListObjectType¶
Recommended for Types
Extends DjangoObjectType with built-in pagination and filtering support.
from django_graphex import DjangoListObjectType
from django_graphex.paginations import LimitOffsetGraphqlPagination
from django.contrib.auth.models import User
class UserListType(DjangoListObjectType):
class Meta:
description = "Type definition for user list"
model = User
pagination = LimitOffsetGraphqlPagination(
default_limit=25,
ordering="-date_joined"
)
filter_fields = {
"username": ("exact", "icontains"),
"email": ("exact", "icontains"),
"is_active": ("exact",),
}
Features¶
- Built-in Pagination: Automatic pagination with configurable settings
- Filtering: Built on Django's ORM lookups +
Qobjects (no django-filter) - Ordering: Custom ordering options
- Caching: Optional query result caching
- Custom Queryset: Override default queryset behavior
Configuration Options¶
class UserListType(DjangoListObjectType):
class Meta:
model = User
description = "User list with advanced features"
# Pagination
pagination = LimitOffsetGraphqlPagination(
default_limit=20,
max_limit=100,
ordering=("-date_joined", "username")
)
# Filtering
filter_fields = {
"username": ("exact", "icontains", "istartswith"),
"email": ("exact", "icontains"),
"date_joined": ("exact", "gte", "lte"),
"is_active": ("exact",),
"groups": ("exact",),
}
# Custom queryset
queryset = User.objects.select_related('profile')
# Field restrictions
fields = ("id", "username", "email", "first_name", "last_name")
exclude = ("password",)
Helper Methods¶
class Query(graphene.ObjectType):
# Get both list and retrieve fields
users = UserListType.ListField(description="List all users")
user = UserListType.RetrieveField(description="Get single user")
DjangoInputObjectType¶
Creates input types for mutations based on Django models.
from django_graphex import DjangoInputObjectType
from django.contrib.auth.models import User
class UserInput(DjangoInputObjectType):
class Meta:
description = "User input for mutations"
model = User
fields = ("username", "email", "first_name", "last_name")
# or exclude specific fields
# exclude = ("password", "date_joined", "last_login")
Advanced Configuration¶
import graphene
from django_graphex import DjangoInputObjectType
class UserCreateInput(DjangoInputObjectType):
"""Input for creating new users"""
# Add custom fields
confirm_password = graphene.String(required=True)
class Meta:
model = User
fields = ("username", "email", "first_name", "last_name", "password")
description = "Input type for user creation"
class UserUpdateInput(DjangoInputObjectType):
"""Input for updating existing users"""
class Meta:
model = User
fields = ("email", "first_name", "last_name")
description = "Input type for user updates"
Usage in Mutations¶
class CreateUserMutation(graphene.Mutation):
class Arguments:
input = UserCreateInput(required=True)
user = graphene.Field(UserType)
success = graphene.Boolean()
def mutate(self, info, input):
# Access input fields
username = input.username
email = input.email
# ... mutation logic
DjangoModelType¶
Recommended for Quick Setup
Automatically generates types, queries, and mutations from a Django model.
All writable model fields are covered, choices become enums, FK fields
accept a pk, and M2M fields accept a list of pks. Partial updates and DB
integrity checks (FK existence, uniqueness, unique_together) are handled
automatically.
from django.contrib.auth.models import User
from django_graphex import DjangoModelType
from django_graphex.paginations import LimitOffsetGraphqlPagination
class UserModelType(DjangoModelType):
class Meta:
description = "User model type with auto-generated operations"
model = User
pagination = LimitOffsetGraphqlPagination(
default_limit=25,
ordering="-date_joined"
)
filter_fields = {
"username": ("exact", "icontains"),
"email": ("exact", "icontains"),
"is_active": ("exact",),
}
Custom base queryset
Pass a Meta.queryset to scope every retrieve/list to a base queryset
(e.g. queryset = User.objects.filter(is_active=True)). It is honored by the
generated RetrieveField() / ListField().
Custom queryset & per-request filtering¶
For anything beyond a static Meta.queryset, override two hooks. info.context
is the request:
from django.db.models import Count, F
class PropertyManagerType(DjangoModelType):
class Meta:
model = PropertyManager
@classmethod
def get_queryset(cls, manager, info, **kwargs):
# custom base queryset for retrieve/list (and mutation responses)
return PropertyManager.objects.select_related("user").annotate(
email=F("user__email"),
lease_count=Count("leases"),
)
@classmethod
def filter_queryset(cls, qs, info, **kwargs):
# per-request scoping; default returns `qs` unchanged
user = info.context.user
if user.is_superuser:
return qs
return qs.filter(company=user.company)
get_queryset(cls, manager, info, **kwargs)supplies the base queryset and appliesfilter_queryset; the default usesMeta.queryset(else the model manager).filter_queryset(cls, qs, info, **kwargs)is the scoping hook; the default is a no-op.
Mutation responses
create / update re-read the mutated object through get_queryset so
annotated/related fields resolve in the response (one extra query). If
filter_queryset would exclude it, the response falls back to the saved
object — a mutation never returns null for what it just wrote.
Custom output fields¶
To expose a field that isn't a plain model column (a model @property, an
annotated value, a computed URL…), declare it directly on the
DjangoModelType.
It is added to the generated output type, so it shows up in both
RetrieveField() and ListField() — no separate DjangoObjectType required:
import graphene
class PropertyManagerType(DjangoModelType):
# Plain graphene fields, resolved from the instance (here from the
# annotations added in get_queryset above, and a model property).
lease_count = graphene.Int(source="lease_count")
email = graphene.String(source="email")
logo_url = graphene.String(source="logo_url") # a model @property
class Meta:
model = PropertyManager
- The field is resolved like any graphene field:
source="x"readsgetattr(instance, "x"), or add aresolve_<name>method for custom logic. - It appears in the detail and the list, because the list reuses the same item type.
resolve_<field> methods¶
A custom field can also be backed by a resolve_<field> method declared on
the DjangoModelType — not just source=. The resolver is forwarded onto the
generated output type, so it runs for both the retrieve and the list. This lets a
custom field return another GraphQL type with arbitrary logic:
import graphene
from django_graphex import DjangoModelType
from myapp.types import PersonType
from myapp.models import Company
class CompanyType(DjangoModelType):
# A computed object field, resolved by the method below.
owner = graphene.Field(PersonType)
class Meta:
model = Company
def resolve_owner(self, info):
# `self` is the Company instance being serialized; return any object
# PersonType can resolve (here, the primary contact).
return self.contacts.filter(is_primary=True).first()
- The method name must be
resolve_<field>where<field>is the attribute name of a custom field declared on the class. It receives(self, info), withselfbound to the model instance andinfo.contextthe request. - The most-derived
resolve_<field>wins, so a subclass can override an inherited resolver by redeclaring the method. - A
resolve_<x>without a matching custom field is ignored (it is not forwarded to the output type).
Custom fields are inherited like normal class attributes, so shared fields can live on an abstract base and a subclass may override one by redeclaring it:
class TimestampedType(DjangoModelType):
age = graphene.String(source="age_display") # shared by subclasses
class Meta:
abstract = True
class InvoiceType(TimestampedType):
total = graphene.Int(source="total_cents") # adds its own
class Meta:
model = Invoice # gets `age` + `total`
Don't mix with a hand-written DjangoObjectType
These two ways are mutually exclusive per model. If a DjangoObjectType is
already registered for the model, the DjangoModelType reuses it and fields
declared here are ignored with a warning — put them on that
DjangoObjectType instead.
Auto-generated Query Fields¶
class Query(graphene.ObjectType):
# Generate both retrieve and list queries automatically
user_retrieve, user_list = UserModelType.QueryFields(
description='User queries',
deprecation_reason='Optional deprecation message'
)
# Or define them separately
user_detail = UserModelType.RetrieveField(
description='Get single user by ID'
)
user_list_custom = UserModelType.ListField(
description='List users with filtering and pagination'
)
Auto-generated Mutation Fields¶
class Mutation(graphene.ObjectType):
# Generate all CRUD mutations
user_create, user_delete, user_update = UserModelType.MutationFields(
description='User CRUD operations'
)
# Or define them separately
create_user = UserModelType.CreateField(description='Create new user')
delete_user = UserModelType.DeleteField(description='Delete user')
update_user = UserModelType.UpdateField(description='Update user')
Custom validation with Pydantic¶
To add field-level validation beyond the automatic DB checks, supply a Pydantic
model via Meta.pydantic_model. Validators use the standard
@field_validator decorator with check_fields=False so the model can be kept
slim:
from pydantic import BaseModel, field_validator
from django.contrib.auth.models import User
from django_graphex import DjangoModelType
class UserValidation(BaseModel):
username: str
email: str
@field_validator("email", check_fields=False)
@classmethod
def email_must_be_corporate(cls, v):
if not v.endswith("@example.com"):
raise ValueError("Only corporate email addresses are accepted.")
return v
class UserModelType(DjangoModelType):
class Meta:
model = User
pydantic_model = UserValidation
Validation errors¶
When a create/update fails validation, the mutation returns ok: false and an
errors list of { field, messages }:
- Errors from nested writes (
Meta.nested_fields) are reported with the nested field name as a prefix —field: "addresses.zip_code"— including nested list children. non_field_errorsis surfaced with an emptyfield(or just the nested model name).
Limiting query shape: max_deep & complexity¶
Every type above accepts two optional Meta options that protect your API from
abusive queries. They are enforced before execution by the validation rules
the library's GraphQLView enables by default. See
Query depth & cost limits for the full reference; the
mini-examples below are the gist.
max_deep — caps how many nested object levels may be selected below a field
returning this type (scalars don't count):
class RentalCompanyType(DjangoModelType):
class Meta:
model = RentalCompany
max_deep = 2 # company -> properties -> units OK; one level deeper is rejected
rentalCompany {
properties { # level 1 ✅
units { # level 2 ✅
tenant { name } # level 3 ❌ "Query exceeds the maximum nesting depth of 2 ..."
}
}
}
complexity — the cost weight of a field returning this type, used by cost
analysis (MAX_QUERY_COST). Make expensive types cost more so a single page of
them eats more of the budget:
class ReportType(DjangoObjectType):
class Meta:
model = Report
complexity = 50 # one report is worth 50; default object weight is 1
Both work on DjangoObjectType, DjangoListObjectType and DjangoModelType
(forwarded to its generated output type). A global depth cap (MAX_QUERY_DEPTH)
and cost budget (MAX_QUERY_COST) are configured in settings — see
Query depth & cost limits.
choices → GraphQL enums¶
Any model field with choices becomes a GraphQL enum automatically — on
every type above (DjangoObjectType, DjangoListObjectType, DjangoModelType).
The interesting part is how each enum member name is chosen, because GraphQL
enum names must be valid identifiers (letters, digits, underscores; not starting
with a digit) and should stay readable and stable across locales.
For each (value, label) choice the name is picked by this cascade:
- The value, if it is already a valid GraphQL name — e.g. a Django
TextChoicesvalue"draft"becomesDRAFT. -
Otherwise the label, resolved as its source msgid with translations off, so the schema is the same in every locale. This is why numeric values with human labels surface readably:
-
Otherwise
A_<value>as a last resort — e.g. a numeric value whose label is empty or also non-identifier-safe yieldsA_1,A_2, …
from django.db import models
from django.utils.translation import gettext_lazy as _
class Profile(models.Model):
# value -> member name
status = models.CharField( # "draft" -> DRAFT (from the value)
max_length=20,
choices=(("draft", "Draft"), ("published", "Published")),
)
gender = models.CharField( # "1"/"2" -> MALE/FEMALE (from the label)
max_length=1,
choices=(("1", _("Male")), ("2", _("Female"))),
)
The enum member's description carries the original label, so the human-readable text is never lost.
Type Comparison¶
| Feature | DjangoListObjectType | DjangoInputObjectType | DjangoModelType |
|---|---|---|---|
| Purpose | List queries with pagination | Input for mutations | Complete CRUD operations |
| Pagination | ✅ Built-in | ❌ N/A | ✅ Built-in |
| Filtering | ✅ Built-in | ❌ N/A | ✅ Built-in |
| Auto Queries | Manual setup | ❌ N/A | ✅ Auto-generated |
| Auto Mutations | ❌ No | ❌ N/A | ✅ Auto-generated |
| Auto-derived Schema | ❌ No | ❌ No | ✅ Full (from model) |
| Customization | High | High | Medium |
| Setup Complexity | Medium | Low | Low |
Best Practices¶
1. Choose the Right Type¶
# ✅ For list queries with custom logic
class UserListType(DjangoListObjectType):
class Meta:
model = User
# ✅ For input validation
class UserInput(DjangoInputObjectType):
class Meta:
model = User
fields = ("username", "email")
# ✅ For rapid prototyping
class UserModelType(DjangoModelType):
class Meta:
model = User
2. Use Descriptive Names¶
# ✅ Clear naming
class UserListType(DjangoListObjectType): pass
class CreateUserInput(DjangoInputObjectType): pass
class UserCRUDType(DjangoModelType): pass
# ❌ Confusing naming
class UserType(DjangoListObjectType): pass # Is it single or list?
class UserInput(DjangoModelType): pass # Not an input type
3. Optimize Performance¶
class UserListType(DjangoListObjectType):
class Meta:
model = User
# Optimize database queries
queryset = User.objects.select_related('profile').prefetch_related('groups')
# Limit exposed fields
fields = ("id", "username", "email", "first_name", "last_name")
# Enable caching for expensive queries
# (Configure in settings)
4. Combine Types Strategically¶
# Use DjangoModelType for basic CRUD
class UserModelType(DjangoModelType):
class Meta:
model = User
# Use DjangoListObjectType for complex list logic
class UserAnalyticsType(DjangoListObjectType):
total_posts = graphene.Int()
class Meta:
model = User
def resolve_total_posts(self, info):
return self.posts.count()
# Use DjangoInputObjectType for complex input validation
class UserRegistrationInput(DjangoInputObjectType):
confirm_password = graphene.String(required=True)
terms_accepted = graphene.Boolean(required=True)
class Meta:
model = User
fields = ("username", "email", "password")