Fields¶
django-graphex provides several field types for building GraphQL schemas with enhanced functionality. Start with the typed scalar descriptors for simple custom fields and arguments, then move to the Django mounting field classes below for Django-model-backed queries.
Typed scalar descriptors¶
For a plain scalar field or argument, the capitalized shortcuts are the
quickest idiom — one shortcut per scalar, usable in both an output
position (an ObjectType body) and an input position (a Mutation's
class Arguments body, or a Field(args=...) mapping).
In an ObjectType, arguments belong on the field descriptor itself, via
Field(args=...):
from django_graphex.core import ObjectType, Field, CharField, IntField
from graphql import GraphQLString
class Query(ObjectType):
greeting = Field(
GraphQLString,
description="a greeting",
args={"name": CharField(default="world"), "loud": IntField(default=0)},
)
def resolve_greeting(self, info, **kwargs):
return f"hello {kwargs['name']}" + ("!" if kwargs["loud"] else "")
In a Mutation, the same shortcuts go in a class Arguments body:
from django_graphex.core import BooleanField, CharField, IntField, Mutation
class Shout(Mutation):
class Arguments:
name = CharField(default="world")
loud = IntField(default=0)
ok = BooleanField()
message = CharField()
@classmethod
def mutate(cls, root, info, **kwargs):
text = f"hello {kwargs['name']}" + ("!" if kwargs["loud"] else "")
return cls(ok=True, message=text)
class Arguments only works inside a Mutation
A class Arguments block nested in a plain ObjectType is silently
ignored — the field compiles with no arguments at all, and the resolver
then fails with KeyError on the argument it expected. Use
Field(args=...) for query arguments.
| Shortcut | GraphQL type |
|---|---|
CharField |
String |
IntField |
Int |
FloatField |
Float |
BooleanField |
Boolean |
IDField |
ID |
DateField |
CustomDate |
DateTimeField |
CustomDateTime |
TimeField |
CustomTime |
DecimalField |
Decimal |
UUIDField |
UUID |
JSONField |
JSON (or JSONString with as_str=True) |
Each shortcut accepts source=, required=, default=, description=,
name=, resolver= and deprecation_reason= — the same kwargs the unified
Field below accepts, minus args= (only Field itself takes args=).
default= only makes sense in an argument position; setting it on an output
field raises a TypeError at schema-build time.
See the full descriptor API reference
for every signature, including JSONField(as_str=True).
The unified Field¶
Field is the one descriptor behind every shortcut above. Reach for it
directly when you need a type the shortcuts don't cover (a DjangoObjectType
reference, an enum, a GraphQLList / GraphQLNonNull wrapper) or when you
need args=, resolver=, or source=:
from django_graphex.core import ObjectType, Field, CharField, IntField
from graphql import GraphQLString
class Query(ObjectType):
# source=: resolves by reading `user_email` off the root
email = Field(GraphQLString, source="user_email")
# args=: an explicit {name: arg} mapping, each value itself a descriptor
greet = Field(
GraphQLString,
description="Greet someone by name",
args={"name": CharField(required=True), "count": IntField(default=1)},
)
def resolve_greet(self, info, **kwargs):
return kwargs["name"] * kwargs["count"]
Field works in both positions, and the parameters that make sense differ by
position:
| Parameter | Position | Meaning |
|---|---|---|
type |
both | The field's type — a graphql-core type, a DjangoObjectType reference (output), or an InputType reference (input). |
required |
both | Wraps the type in non-null (T!). |
description |
both | Field / argument description. |
name |
both | Explicit wire name (skips camelCase on output — in every output position, root or nested; drives out_name on input). |
deprecation_reason |
both | Marks the field / argument @deprecated(reason: ...). |
source |
output only | Resolve by reading an attribute off the root. |
resolver |
output only | Field-level resolver (wins over the parent resolver). |
args |
output only | Explicit {name: arg} mapping for the field's arguments. |
default |
input only | The GraphQL default value; omit to leave the argument with no default. |
Setting an output-only parameter (resolver=, source=, args=) on a
Field used inside a class Arguments body raises a TypeError naming the
offending kwarg. Setting default= on a Field used in an ObjectType body
raises a TypeError at output compile time. There is no separate
InputField — the same Field (and the same typed shortcuts) work on both
sides.
name= is the escape hatch for an attribute name that collides with a Python
keyword. It is honoured wherever the field is declared — on a root, on a
mutation payload, and on any nested ObjectType:
Django mounting fields¶
The field classes below wire a DjangoObjectType / DjangoListObjectType
into a Query, from simplest to most capable: DjangoObjectField for a
single object, then list fields with progressively more built-in filtering
and pagination.
DjangoObjectField¶
Used for single object queries with automatic ID filtering.
from django_graphex.fields import DjangoObjectField
from django_graphex.core import ObjectType
from .types import UserType
class Query(ObjectType):
user = DjangoObjectField(UserType, description='Single User query')
Features: - Automatic ID-based filtering - No need to define custom resolve function - Built-in error handling for non-existent objects
Usage in GraphQL:
Every mounting field class below accepts deprecation_reason=, which renders
as @deprecated(reason: ...) on the compiled field:
class Query(ObjectType):
user = DjangoObjectField(
UserType,
deprecation_reason="Use `activeUser` instead.",
)
DjangoFilterListField¶
Provides filtering capabilities for list queries without pagination.
from django_graphex.fields import DjangoFilterListField
from django_graphex.core import ObjectType
from .types import UserType
class Query(ObjectType):
users = DjangoFilterListField(UserType)
Features:
- Built on Django's ORM lookups + Q objects (no django-filter)
- Multiple filter types (exact, contains, etc.)
- No pagination (returns all matching results)
Usage in GraphQL:
DjangoFilterPaginateListField¶
Combines filtering and pagination for list queries.
from django_graphex.fields import DjangoFilterPaginateListField
from django_graphex.core import ObjectType
from django_graphex.paginations import LimitOffsetGraphqlPagination
from .types import UserType
class Query(ObjectType):
users = DjangoFilterPaginateListField(
UserType,
pagination=LimitOffsetGraphqlPagination(default_limit=20)
)
Features: - All filtering capabilities of DjangoFilterListField - Built-in pagination support - Configurable pagination class
Usage in GraphQL:
{
users(filter: { firstName: { icontains: "john" } }, limit: 10, offset: 0) {
id
username
firstName
}
}
Flat list
DjangoFilterPaginateListField returns a flat list, so both the filter
arguments and the pagination/ordering arguments (limit, offset,
ordering) live directly on the list field. There is no results /
totalCount wrapper — for that, use DjangoListObjectField.
Mounted on a type: one relation must scope the list
Mounted on a DjangoObjectType (rather than on Query), the field scopes
its rows to the parent row through the relation that points back at the
parent. That works only while exactly one relation does: a child with
created_by and updated_by foreign keys to the same parent — or a
foreign key alongside a many-to-many — is ambiguous, and the library now
refuses it with ImproperlyConfigured naming both relations rather than
guessing. It used to apply all of them at once, which is a conjunction:
the list silently resolved to [] for every parent that was not on both
sides of every row.
For an ambiguous child, mount the nested list through its relation
accessor instead — the auto-generated nested list field, or an explicit
DjangoNestedListObjectField(ArticleListType, accessor="created_articles").
Reading the accessor names the relation outright, so nothing has to be
inferred.
DjangoListObjectField¶
Recommended for Queries
This is the most flexible approach for list queries with built-in support for filtering and pagination.
from django_graphex.fields import DjangoListObjectField
from django_graphex.core import ObjectType
from .types import UserListType
class Query(ObjectType):
users = DjangoListObjectField(UserListType, description='All Users query')
Features:
- Works with DjangoListObjectType
- Inherits pagination configuration from the type
- Filtering via the type's filter_fields (built on Django ORM lookups + Q)
- Built-in caching support
Usage in GraphQL:
Filter arguments live on the list field; pagination and ordering arguments
(limit, offset, ordering) live on the results subfield. totalCount is
a sibling of results.
{
users(filter: { isActive: { exact: true } }) {
results(limit: 10, offset: 0, ordering: "-id") {
id
username
firstName
}
totalCount
}
}
Custom filtering logic¶
There are no FilterSet classes. Declarative lookups come from the type's
Meta.filter_fields and are queried through the nested filter: argument. For
bespoke rules (e.g. a free-text search across several columns), override
get_queryset / filter_queryset on a DjangoModelType:
from django.db.models import Q
from django_graphex.types import DjangoModelType
from django.contrib.auth.models import User
class UserType(DjangoModelType):
class Meta:
model = User
filter_fields = {"username": ("icontains",), "email": ("icontains",)}
@classmethod
def filter_queryset(cls, qs, info, **kwargs):
term = info.context.GET.get("q") if hasattr(info.context, "GET") else None
if term:
qs = qs.filter(
Q(first_name__icontains=term) | Q(last_name__icontains=term)
)
return qs
See Permissions & hooks and the Filtering guide.
AnnotatedField¶
A field backed by a Django ORM annotation that is injected into the
queryset only when the field is selected in the GraphQL query. A built-in
default resolver reads the annotated value off the row, so no resolve_<field>
is needed — and when the field is not selected, no annotation (and no extra SQL)
is added at all.
AnnotatedField is public — import it directly:
Signature:
Example — a per-author post count, computed in the database:
from graphql import GraphQLInt
from django.db.models import Count
from django_graphex.fields import AnnotatedField
from django_graphex.types import DjangoObjectType
class AuthorType(DjangoObjectType):
# Backed by a DB annotation, injected ONLY when `postCount` is selected.
post_count = AnnotatedField(GraphQLInt, Count("posts"))
class Meta:
model = Author
Usage in GraphQL:
When postCount is selected, the optimizer adds the Count("posts")
annotation to the queryset. When it is not selected, the annotation is never
injected and the query is unchanged.
Arguments:
| Argument | Meaning |
|---|---|
type_ |
The graphql-core output type (e.g. GraphQLInt). |
expression |
A Django Expression instance or a zero-arg callable returning one. The callable is invoked lazily at injection time, per request — useful for constructing a fresh expression on each resolve. |
aliases |
Optional dict[str, Expression \| callable] applied via .alias() before .annotate() (for intermediate expressions the annotation depends on). |
annotation_name |
Overrides the auto-derived annotation key. Defaults to _gqx_ann_<snake_field>. Set it when the auto key would collide with a model attname. |
# expression may also be a zero-arg callable for fresh construction per request:
post_count = AnnotatedField(GraphQLInt, lambda: Count("posts"))
Selection-driven and gated by a setting
Injection runs as qs.alias(**aliases).annotate(**{annotation_name: expression})
and only fires when the field appears in the client selection set and
OPTIMIZE_ANNOTATED_FIELDS is True (the default). Set it to False to
disable AnnotatedField injection entirely. The default resolver returns
None when the annotation was not injected (field not selected).
Annotations across a forward FK
Child AnnotatedFields on a nested type are injected on that child's
Prefetch queryset. A forward-FK relation whose child selects an
AnnotatedField is auto-promoted from select_related to
prefetch_related, because DB annotations cannot be pushed through a SQL
JOIN. See
Query Optimization → Selection-driven annotations
for a worked example.
Custom resolvers¶
DjangoObjectField, DjangoFilterListField, DjangoFilterPaginateListField and
DjangoListObjectField accept a custom resolver=. When given, it is used
instead of the built-in resolver — but it still receives the library's plumbing as
its leading positional arguments, so you can reuse filtering/pagination and
only change the base queryset:
- single object:
resolver(manager, root, info, **kwargs) - lists:
resolver(manager, filter_backend, root, info, **kwargs)
def active_users(manager, root, info, **kwargs):
# custom base queryset; manager is the model's default manager
return manager.filter(is_active=True).get(pk=kwargs["id"])
field = DjangoObjectField(UserType, resolver=active_users)
This is also what powers DjangoModelType.RetrieveField() / ListField()
(which inject cls.retrieve / cls.list), so a DjangoModelType.Meta.queryset
is honored by its list/retrieve.
Field Comparison¶
| Feature | DjangoObjectField | DjangoFilterListField | DjangoFilterPaginateListField | DjangoListObjectField |
|---|---|---|---|---|
| Single Objects | ✅ | ❌ | ❌ | ❌ |
| List Objects | ❌ | ✅ | ✅ | ✅ |
| Filtering | ID only | ✅ | ✅ | ✅ |
| Pagination | ❌ | ❌ | ✅ | ✅ |
| Custom queryset hooks | ❌ | ✅ | ✅ | ✅ |
| Type Integration | Basic | Basic | Basic | Full |
| Caching | ❌ | ❌ | ❌ | ✅ |
DjangoNestedListObjectField¶
DjangoNestedListObjectField is the field class used automatically when the
optimizer wires a reverse-FK or M2M relation. You can also declare it
explicitly on a parent DjangoObjectType to expose a nested list with custom
accessor, filter, or pagination behavior.
DjangoNestedListObjectField is part of the public API — import it directly:
Signature:
| Argument | Meaning |
|---|---|
list_type |
A DjangoListObjectType subclass describing the nested list's shape, pagination, and filter options. |
accessor |
The parent attribute name for the relation (defaults to the field name on the parent type). |
fields |
Override filter configuration. Defaults to list_type._meta.filter_fields. Pass None to inherit from the type; pass {} or an empty dict to disable filtering on this field. |
**kwargs |
Forwarded to the underlying Field. |
Example — expose a per-author paginated list of posts:
from django_graphex.fields import DjangoNestedListObjectField
from django_graphex.paginations import LimitOffsetGraphqlPagination
from django_graphex.types import DjangoObjectType, DjangoListObjectType
from .models import Author, Post
class PostType(DjangoObjectType):
class Meta:
model = Post
filter_fields = {"title": ["icontains"]}
class PostListType(DjangoListObjectType):
class Meta:
model = Post
pagination = LimitOffsetGraphqlPagination(default_limit=10, ordering="-id")
filter_fields = {"title": ["icontains"]}
class AuthorType(DjangoObjectType):
posts = DjangoNestedListObjectField(PostListType)
class Meta:
model = Author
Usage in GraphQL:
{
author(id: 1) {
name
posts(filter: { title: { icontains: "graphql" } }) {
results(limit: 5, ordering: "-id") {
id
title
}
totalCount
}
}
}
The optimizer automatically uses prefetch_related (with DB-side window slicing
when OPTIMIZE_NESTED_PAGINATION is True). No additional select_related or
prefetch_related calls are needed — declaring the field is enough.
See Query Optimization → DB-side nested pagination and Nested Lists for the full reference.
Best Practices¶
1. Use DjangoListObjectField for Lists¶
DjangoListObjectField pairs with DjangoListObjectType to give you
pagination, filtering, and totalCount out of the box — without writing a
resolver. DjangoFilterPaginateListField returns a flat list with no wrapper
shape, which limits client-side pagination controls.
from django_graphex.core import ObjectType
# ✅ Recommended
class Query(ObjectType):
users = DjangoListObjectField(UserListType, description='All users')
# ❌ Less flexible — no totalCount, no results wrapper
class Query(ObjectType):
users = DjangoFilterPaginateListField(UserType)
2. Define Filter Fields in Types¶
Declaring filter_fields on the type (rather than the field) makes the filter
available everywhere the type is used — list fields, nested lists, and
RetrieveField — without repeating the configuration.
class UserType(DjangoObjectType):
class Meta:
model = User
filter_fields = {
"username": ("exact", "icontains"),
"email": ("exact", "icontains"),
"is_active": ("exact",),
}
3. Use Descriptive Names¶
Descriptive names make the schema self-documenting and reduce the need for
separate API docs. Include intent in the name (active_users > users) and
always add a description=.
from django_graphex.core import ObjectType
class Query(ObjectType):
# ✅ Clear and descriptive
active_users = DjangoListObjectField(
UserListType,
description='List of active users with pagination'
)
user_by_id = DjangoObjectField(
UserType,
description='Get a single user by ID'
)
4. Combine with Permissions¶
Keep permissions out of the schema layer — put them in a resolver= or
filter_queryset hook so the rule is enforced regardless of which field
triggers the query. This also keeps the field declaration clean and
independently testable.
Pass a custom resolver= that reuses the library's plumbing (it receives
manager, filter_backend, root, info, **kwargs for list fields) and only changes
the base queryset; filter_backend.apply(qs, kwargs.get("filter")) applies the
nested filter: argument:
from django_graphex.fields import DjangoListObjectField
from django_graphex.core import ObjectType
from django_graphex.base_types import DjangoListObjectBase
from graphql import GraphQLError
def staff_only_users(manager, filter_backend, root, info, **kwargs):
if not info.context.user.is_staff:
raise GraphQLError("Staff access required")
qs = manager.get_queryset()
qs = filter_backend.apply(qs, kwargs.get("filter"))
return DjangoListObjectBase(count=qs.count(), results=qs)
class Query(ObjectType):
users = DjangoListObjectField(UserListType, resolver=staff_only_users)