Types API Reference¶
This section provides detailed API documentation for GraphQL type classes in django-graphex.
Field descriptors¶
Capitalized field descriptors for declaring custom (non-model) fields and
mutation arguments. All are imported from django_graphex.core. See
Declaring fields: the descriptor API
for the usage guide.
Field (both positions)¶
Field(type, *, source=None, required=False, default=_UNSET, description=None,
name=None, resolver=None, args=None, deprecation_reason=None)
One descriptor for both positions — an ObjectType / Mutation payload
body (output) and a class Arguments body or Field(args={...}) /
field(args={...}) mapping (input). Direction comes from the declaration site,
never from the descriptor. type takes:
- Output: any graphql-core output type, a
DjangoObjectTypereference (resolved lazily), or aNativeList/NativeNonNullwrapper. - Input: a
DjangoInputObjectType/InputTypeCLASS (resolved lazily to its compiled input type) or a bare graphql-core scalar.
| Keyword | Effect |
|---|---|
source="attr" |
Output only. Resolve by reading attr off the root (or calling it if callable). |
required=True |
Wrap the type in non-null (T!) — lazy on output, eager on input. |
default=value |
Input only. GraphQL argument default; omit it (the _UNSET sentinel) to leave the argument with no default, or pass default=None for a real null default. |
description |
Field / argument description. |
name |
Explicit wire name (skips camelCase). |
resolver |
Output only. Field-level resolver (wins over the parent). |
args |
Output only. Explicit {name: arg} mapping (each value may be a Field). |
deprecation_reason |
Renders @deprecated(reason: ...) on the field / argument. |
Wrong-position keywords fail loud: an output-only source= / resolver= /
args= set on a field used in an argument position raises a clear TypeError;
the input-only default= set in an output position raises a TypeError at
output compile.
Scalar shortcuts¶
There are 11 shortcuts, each binding one scalar and each position-agnostic
(usable in an ObjectType body and a class Arguments body). Each returns a
Field. Signature:
(*, source=None, required=False, default=_UNSET, description=None, name=None, resolver=None, deprecation_reason=None)
(JSONField additionally takes as_str=False).
| Shortcut | GraphQL type |
|---|---|
IDField |
ID |
IntField |
Int |
FloatField |
Float |
BooleanField |
Boolean |
CharField |
String |
DateField |
CustomDate |
DateTimeField |
CustomDateTime |
TimeField |
CustomTime |
DecimalField |
Decimal (string, plain notation, stored scale kept: 0.00000050, never 5.0E-7; a value needing more than 1000 padded zeros keeps scientific form) |
UUIDField |
UUID |
JSONField |
JSON (or JSONString with as_str=True) |
The 12 former *InputField twins (CharInputField, IntInputField, …) and the
standalone InputField descriptor are removed in 2.0 — the single Field
and these shortcuts cover both positions.
Date/time scalars and JSON
The date/time shortcuts render as CustomDate / CustomDateTime /
CustomTime (v1 SDL parity; the plain Date / DateTime / Time names
belong to the filter-lookup scalars). JSONField() binds the raw JSON
scalar (structured JSON on the wire); JSONField(as_str=True) binds the
string-encoded JSONString escape hatch. See
Accepted date/time input formats
and JSONField → JSON.
Import from django_graphex.core, not django.db.models
django.db.models also exports CharField, IntegerField, etc. Declaring a
model field where a descriptor is expected raises a loud TypeError
naming the likely import mistake — on both the type-body and mutation-argument
paths. Always import descriptors from django_graphex.core.
field() is the low-level substrate
The descriptors are sugar over the public field(type, *, description=None,
args=None, resolver=None, name=None, required_perms=None) helper, which stays
public and unchanged. field() has no source= / required= (those live on
the descriptors); use it for the raw graphql-core-typed primitive.
DjangoObjectType¶
Enhanced Django model GraphQL type with filtering and pagination support.
Meta Configuration¶
The DjangoObjectType is configured through a nested Meta class:
class UserType(DjangoObjectType):
class Meta:
model = User
only_fields = ('id', 'username', 'first_name', 'last_name')
filter_fields = {'username': ('exact', 'icontains')}
Keep Django account types read-only
The User projection above is deliberately allow-listed. Mount account
reads through AuthenticatedGraphQLView and disable response caching on
this session-aware path:
Do not generate create, update or delete fields for Django's user model.
Put registration in a purpose-built mutation with a narrow input and call
User.objects.create_user(username=..., password=...); omit staff,
superuser, group and permission inputs. See the executable
Quick Start.
Meta Options¶
| Option | Type | Default | Description |
|---|---|---|---|
model |
Model |
Required | Django model class |
registry |
Registry |
Global registry | Type registry instance |
skip_registry |
bool |
False |
Skip automatic type registration |
only_fields |
tuple/list |
() |
Include only specified automatically derived model fields. Declared output names remain valid selectors but declarations compile independently. Unknown names and bare strings fail at class definition. A security boundary for model projection — see below |
exclude_fields |
tuple/list |
() |
Exclude specified automatically derived model fields. Declared output names remain valid selectors but this option does not remove declarations. Unknown names and bare strings fail at class definition. A security boundary for model projection — see below |
include_fields |
tuple/list |
() |
Additional fields to include |
filter_fields |
dict |
None |
Field filtering configuration. An entry naming a projected-away column fails the schema build |
interfaces |
tuple |
() |
GraphQL interfaces to implement |
description |
str |
None |
GraphQL description for this type; defaults to the class docstring when omitted |
unions |
dict |
None |
Mapping of GenericForeignKey field name → DjangoUnionType subclass; enables typed GFK targets instead of GenericForeignKeyType. Renamed from gfk_unions in 2.0 — the old key raises ImproperlyConfigured. |
max_depth |
int |
None |
Max nested-object depth below this type (see Query depth limiting) |
complexity |
int |
None |
Cost weight of a field returning this type (see Query cost analysis) |
An automatically derived model field with choices uses a generated GraphQL
enum. If two choices generate the same member name, compiling the affected
type raises ImproperlyConfigured instead of assigning a positional suffix.
Adjust the choices so their generated names differ; see
Generated enum member collisions.
only_fields / exclude_fields are a security boundary
A column this type projects away must not be readable, orderable or
filterable through it. ordering rejects it at query time; a
filter_fields entry naming it stops the schema from building. The rule,
what "hidden" means when a declaration or a relation is involved, its one
exception and the two boundaries it cannot close are stated once, in
The projection is a security boundary.
Write complete field names in a collection: exclude_fields = ("password",),
not exclude_fields = "password". A typo such as "passwrod" now raises
ImproperlyConfigured before the type registers. Model fields, reverse
relation names, and explicitly declared output fields are valid names;
a declared output field is still compiled independently of model projection.
Unknown Meta options raise ImproperlyConfigured
Any key not in the table above is rejected at server startup with
django.core.exceptions.ImproperlyConfigured. A previously silent typo
(e.g. filter_Filed) now surfaces immediately. See also
Meta options are validated.
Methods¶
resolve_id(info)¶
Resolve the ID field for the object.
Returns: Primary key value of the model instance
is_type_of(root, info) (classmethod)¶
Check if the root object is of this type.
Parameters:
- root (Any): Object to check
- info (ResolveInfo): GraphQL resolve info
Returns: bool - True if object matches this type
get_queryset(queryset, info) (classmethod)¶
Override to customize the queryset used for this type. Queryset-backed root and
mounted list/node paths use this hook. Generated FK/M2M mutation ID links also
check the registered destination type's hook in the request schema before
persisting the parent. A hidden link ID returns the same structured
Invalid pk error as a nonexistent ID; see
mutation link migration.
Automatically expanded relation attributes and ORM relation-filter joins do
not invoke this hook; see the row-scope boundary.
Parameters:
- queryset (QuerySet): Base queryset
- info (ResolveInfo): GraphQL resolve info
Returns: Modified QuerySet
Raises: TypeError if the override returns anything other than a QuerySet
(the scope cannot be honoured, so the request is denied instead of serving
unscoped rows)
get_node(info, id) (classmethod)¶
Get a single node by ID. The lookup runs through get_queryset, so a row the
scope excludes is reported as missing — the ID comes straight from the caller.
Parameters:
- info (ResolveInfo): GraphQL resolve info
- id (Any): Object identifier
Returns: Model instance or None
Raises: GraphQLError with extensions.code=INVALID_ID when the model primary key cannot convert the ID.
Example Usage¶
class UserType(DjangoObjectType):
class Meta:
model = User
only_fields = ('id', 'username', 'first_name', 'last_name')
@classmethod
def get_queryset(cls, queryset, info):
return queryset.select_related('profile').prefetch_related('posts')
A prefetch here for a relation the optimizer also derives from the selection is replaced by the derived (narrowed, filtered) version rather than colliding with it; prefetches of other relations are kept. See Query Optimization.
DjangoInputObjectType¶
Django model GraphQL input type for mutations and arguments.
Meta Configuration¶
Configure input types through the Meta class:
class ArticleInput(DjangoInputObjectType):
class Meta:
model = Article
input_for = 'create'
only_fields = ('title', 'body', 'status')
Meta Options¶
| Option | Type | Default | Description |
|---|---|---|---|
model |
Model |
Required | Django model class |
registry |
Registry |
Global registry | Type registry instance |
skip_registry |
bool |
False |
Skip automatic registration |
only_fields |
tuple/list |
() |
Include only specified fields |
exclude_fields |
tuple/list |
() |
Exclude specified fields |
filter_fields |
dict |
None |
Field filtering configuration |
input_for |
str |
'create' |
Input purpose: 'create', 'update', or 'delete' |
include_fields |
tuple/list |
() |
Additional fields to include |
nested_fields |
dict |
() |
Nested field configuration: {accessor_name: ChildModel}. Anything that is not a dict is ignored |
nested_parent_model |
Model |
None |
Mark this input as the nested child of that model: its back-reference ForeignKey / OneToOneField becomes optional |
container |
type |
Auto-generated | Container class for the input type |
nested_parent_model is what makes a nested payload writable without repeating
the parent's id: the nested writer injects that key at save time, so requiring
it inline would make every nested create unsatisfiable. It is set automatically
on the per-parent child input a Meta.nested_fields entry builds (see
the nested child input type);
set it by hand only on an input you mount yourself inside another model's input.
The child's Pydantic validation model still requires the key, so a standalone
create that omits it fails cleanly.
An existing unowned nullable reverse-FK child named by ID needs a registered update-serving child write host before its parent may adopt it. New children and already-owned child updates retain their prior behavior; see the orphan-adoption contract.
Methods¶
get_type() (classmethod)¶
Get the type when the unmounted type is mounted.
Returns: The input type class
Example Usage¶
A delete input is keyed on the model's real primary key, so a model
whose pk is a UUIDField, a SlugField or any other renamed field works
exactly like one using the default id.
class ArticleInput(DjangoInputObjectType):
class Meta:
model = Article
input_for = 'create'
# {accessor name: child model} — the relations to expand
# into nested input objects
nested_fields = {'profile': Profile, 'addresses': Address}
nested_fields must be a dict: the accessor alone does not say which
model the child input is built from, so a tuple such as
('profile', 'addresses') is ignored and the relations keep their plain
ID / [ID!] surface.
An all-string Literal field on a hand-authored InputType compiles to an
enum named from the compiled input type and the public field alias, such as
ArticleInputStatusEnum. Its members use the existing choice-name
normalization ("draft" becomes DRAFT); two values with the same generated
member name fail schema construction. Inline documents and JSON variables
send member names, while resolvers receive the original string values.
Nullable/list/nested wrappers retain their usual input shape. Numeric or
mixed Literal annotations still warn and use the String fallback.
Only direct typing.Union[int, str] / int | str (either member order)
compile to one shared IntOrString input scalar. Required and outer nullable
forms publish IntOrString! and IntOrString, respectively. Integers retain
their Python type and must fit signed 32-bit range; strings remain strings.
There is no float, boolean, string-to-number, or number-to-string conversion.
Invalid supplied values reject before the mutation resolver. Omitted fixed and
factory defaults retain their prior behavior, including malformed legacy
values. Other Union shapes, including list-wrapped unions, retain their warning
and String fallback. Existing global scalar mappings and output types do not
change.
The exact dict[str, int] and typing.Dict[str, int] annotations compile
to the existing raw JSON input scalar, preserving the same scalar identity as a bare dict. Unlike
bare dict, it accepts only objects with string keys and plain integer
values, without Pydantic's numeric-string or boolean coercion. Values follow Python int and JSON semantics
(there is no 32-bit GraphQL Int bound). Its optional,
list, nested and default wrappers keep their existing behavior.
Exact dict[str, str] and typing.Dict[str, str] fields use that same
JSON scalar and require string keys and string values, without implicit
conversion. They retain the existing optional, list, nested, default, and
permission-pruned input behavior. Other parameterized dictionaries remain
unsupported and use the warning/String fallback; a bare dict remains
permissive raw JSON.
Exact dict[str, bool] and typing.Dict[str, bool] direct mutation inputs now
use the same JSON scalar instead of String. Send a JSON object such as
"mapping": {"enabled": true} or inline mapping: {enabled: true};
encoded JSON strings no longer match the wire type. Every key must be a
string and every value must be an exact Boolean (true or false): 1,
0, "true", null and nested values fail before the resolver or omitted
sibling factories run. Empty objects are valid. Outer Optional, alias and
fixed/factory-default behavior, including malformed omitted legacy defaults,
is unchanged. This is field-local validation, not a change to bare dict,
other maps, list-wrapped maps or the global JSON scalar.
Nested list/typing.List wrappers around already-supported input leaves
retain their annotated nullability at every level. For example,
list[list[int]] compiles to [[Int!]!]!; GraphQL accepts nested values and
wraps singleton values at each list level without casting leaf values.
Strict typed-map validation still runs at the final JSON leaf and reports
safe indexed field paths. Nested wrappers around unsupported leaves retain
their existing warning/String fallback instead of gaining a new input type.
The exact set[int] and typing.Set[int] input annotations expose [Int!]!.
GraphQL accepts an integer list or a singleton integer and validates each
value with GraphQLInt before resolution. The resolver receives a Python set:
duplicate values are removed, and set iteration order is unspecified.
Existing outer optionality and defaults are retained. Other set leaves and
sets nested inside lists retain their legacy fallback.
Exact set[str] and typing.Set[str] mutation inputs expose
[String!]!. GraphQL accepts a string list or a whole-string singleton and
validates each item with native GraphQLString. The resolver receives a
deduplicated Python set: ["red", "red", "blue"] becomes
{"red", "blue"}, while "ab" becomes {"ab"}, not two characters.
No iteration order is guaranteed. Existing outer optionality and defaults
remain; other unapproved set leaves and nested sets still retain fallback behavior.
Exact set[bool] and typing.Set[bool] mutation inputs expose
[Boolean!]!. GraphQL accepts a boolean list or a singleton such as
false, and validates each item with native GraphQLBoolean: 0, 1,
strings and null list items are rejected before resolution. The resolver
receives a deduplicated Python set, so false becomes {False}; no
iteration order is guaranteed. Existing outer optionality, defaults,
aliases and permission pruning remain. Other unapproved set leaves and nested sets
retain their fallback behavior.
Exact set[float] and typing.Set[float] mutation inputs expose
[Float!]!. GraphQL accepts finite numeric lists or singleton numbers,
coercing integers to native float values, while rejecting booleans, strings
and null list items. Variables reject nonfinite values; a field-local check
rejects overflowing numeric literals such as 1e999 before resolution.
The resolver receives
a deduplicated Python set: [1.5, 1.5, 2.0] becomes {1.5, 2.0} and
2 becomes {2.0}. No iteration order is guaranteed; outer optionality,
defaults, aliases and permission pruning retain their existing behavior.
Unapproved set leaves and nested sets retain fallback behavior.
Exact set[uuid.UUID] and typing.Set[uuid.UUID] mutation inputs expose
[UUID!]!. The existing native UUID scalar accepts canonical, uppercase and
32-hex UUID strings in a list or as a whole-string singleton, validates each
item before resolution, and supplies UUID objects. The resolver receives a
deduplicated Python set; order is not guaranteed. Invalid UUIDs and null
items reject before resolution. Existing outer optionality, defaults, aliases
and permission pruning remain. Other unapproved set leaves and nested sets
retain fallback behavior.
Exact set[decimal.Decimal] and typing.Set[decimal.Decimal] mutation inputs
expose [Decimal!]! using the existing native Decimal scalar. Finite native
values, including a huge finite exponent, keep their exact precision through
list or singleton coercion; ["10.10", "10.10"] reaches the resolver as the
Python set {Decimal("10.10")} without an ordering promise. Supplied NaN,
sNaN and either Infinity are rejected by a field-local check before resolver
execution or omitted sibling factories. Outer optionality, fixed/factory
defaults, aliases and pruned input fields retain their established behavior;
malformed legacy omitted defaults are not newly validated. The global Decimal
scalar and unapproved set leaves remain unchanged.
A fixed JSON default with GraphQL Name object keys, such as
{"views": 0}, now renders through print_schema and default-value
introspection as mapping: JSON = { views: 0 }. It still reaches the resolver
for an omitted input as the original raw value. Nested representable JSON
values work the same way. A fixed object with a key such as "x-y" keeps its
existing runtime default but cannot be printed as a valid GraphQL object
literal. A valid fixed list of JSON values, including a correctly shaped
nested list, also renders in SDL and introspection. Its omitted input receives
an equal list value; raw JSON leaves retain their existing identity. Malformed
list defaults retain their legacy runtime and metadata behavior.
Where metadata cannot render,
use Field(default_factory=lambda: {"views": 0}): it supplies a value for each
omitted input without advertising a fixed SDL default. A nullable None
default is unaffected. Cyclic and non-JSON-native values, strings containing
unpaired Unicode surrogates, and integers beyond Python's configured
integer-to-string limit gain no new metadata support; runtime defaults remain
unchanged.
Pydantic defaults on a hand-authored InputType
A plain default (limit: int = 10) remains a fixed SDL default
(limit: Int = 10). A zero-argument default_factory is different: its
field has no SDL default, and the factory runs for each new GraphQL-
coerced input object that omits the field. For example, uid: str =
Field(default_factory=new_uid) renders as uid: String, not a fixed
uid: String = "uid-1". Two separate omitted inputs receive fresh values;
two aliases referencing the same variable input object share that one
coerced value. Explicit values and null do not invoke the factory.
Resolver input remains a plain dict with snake-case field names. A
factory returning a list now supplies a fresh list per new input object,
rather than a mutable list shared across requests. A factory error becomes
a GraphQL input error without exposing the original exception text.
The one exception is Pydantic 2.10+'s validated-data factory
(Field(default_factory=lambda data: data["a"] + 1)): it needs the
partially validated instance, so GraphQL cannot evaluate it; the field
renders with no = … marker and Pydantic still applies it when the
input is explicitly validated. Model-derived mutation inputs continue to
omit synthetic defaults so omitted updates remain distinct from null.
DjangoListObjectType¶
GraphQL type for paginated lists of Django objects with count and results.
Meta Configuration¶
Configure list types with pagination and filtering:
class UserListType(DjangoListObjectType):
class Meta:
model = User
pagination = LimitOffsetGraphqlPagination(default_limit=25)
Meta Options¶
| Option | Type | Default | Description |
|---|---|---|---|
model |
Model |
Required | Django model class |
registry |
Registry |
Global registry | Type registry instance |
only_fields |
tuple/list |
() |
Include only specified fields |
exclude_fields |
tuple/list |
() |
Exclude specified fields |
include_fields |
tuple/list |
() |
Additional fields to include |
queryset |
QuerySet |
None |
Base queryset for the list; used as a template — every request runs a fresh clone, so it never caches rows |
description |
str |
Auto-generated | Type description |
results_field_name |
str |
'results' |
Name of results field |
pagination |
BaseDjangoGraphqlPagination |
None |
Pagination configuration |
filter_fields |
dict |
None |
Field filtering configuration |
max_depth |
int |
None |
Max nested-object depth below this list type (see Query depth limiting) |
complexity |
int |
None |
Cost weight of a field returning this list type (see Query cost analysis) |
Generated Fields¶
A DjangoListObjectType automatically generates these fields:
| Field | Type | Description |
|---|---|---|
totalCount |
Int |
Total number of objects |
results |
List[ObjectType] |
Paginated list of objects |
Pagination and ordering arguments (limit, offset, page, pageSize, first,
ordering, cursor arguments) are placed on the results(...) subfield. totalCount
(and pageInfo, for cursor pagination) are siblings of results. Filter arguments
are placed on the list field itself.
Methods¶
RetrieveField(**kwargs) (classmethod)¶
Create a field for retrieving a single object from this list type.
Parameters:
- **kwargs: Additional field arguments
Returns: DjangoObjectField instance
BaseType() (classmethod)¶
Return the base DjangoObjectType wrapped by this list type.
Returns: The base object type class
Example Usage¶
from django_graphex.paginations import PageGraphqlPagination
class PostListType(DjangoListObjectType):
class Meta:
model = Post
pagination = PageGraphqlPagination(
page_size=15,
page_size_query_param='pageSize'
)
filter_fields = {
'title': ('icontains',),
'status': ('exact',),
'author__username': ('icontains',),
}
from django_graphex.fields import DjangoListObjectField
from django_graphex.core import ObjectType
class UserListType(DjangoListObjectType):
class Meta:
model = User
pagination = LimitOffsetGraphqlPagination(default_limit=25)
class Query(ObjectType):
active_users = DjangoListObjectField(UserListType)
def resolve_active_users(self, info, **kwargs):
# A `resolve_<field>` returning a QuerySet is honored by
# `queryset_factory`, scoping the list's base queryset.
return User.objects.filter(is_active=True).select_related('profile')
from django_graphex.fields import DjangoListObjectField
from django_graphex.core import ObjectType
from django_graphex.schema import DjangoGraphQLSchema
class Query(ObjectType):
# Preferred: DjangoListObjectField takes the list type directly
all_users = DjangoListObjectField(UserListType)
# RetrieveField() shorthand is available on DjangoListObjectType
user = UserListType.RetrieveField()
schema = DjangoGraphQLSchema(query=Query)
ListField() is not available on DjangoListObjectType
UserListType.ListField() raises AttributeError — ListField is a
classmethod on DjangoModelType, not on DjangoListObjectType. Use
DjangoListObjectField(UserListType) instead.
GraphQL Response Structure¶
{
"data": {
"allUsers": {
"totalCount": 150,
"results": [
{
"id": "1",
"username": "john_doe",
"email": "john@example.com"
},
{
"id": "2",
"username": "jane_smith",
"email": "jane@example.com"
}
]
}
}
}
DjangoModelType¶
GraphQL type with automatic CRUD operations driven directly by a Django model.
Meta Configuration¶
Configure model types with automatic CRUD operations:
class ArticleType(DjangoModelType):
class Meta:
model = Article
pagination = LimitOffsetGraphqlPagination(default_limit=25)
Meta Options¶
| Option | Type | Default | Description |
|---|---|---|---|
model |
Model |
Required | Django model class |
pydantic_model |
BaseModel |
Auto-generated | Pydantic model for custom validation; auto-generated from model when omitted |
queryset |
QuerySet |
None |
Base queryset for retrieve and list operations; honored by get_queryset |
pagination |
BaseDjangoGraphqlPagination |
None |
Pagination configuration |
only_fields |
tuple/list |
() |
Include only specified fields |
include_fields |
tuple/list |
() |
Additional fields to include |
exclude_fields |
tuple/list |
() |
Exclude specified fields |
input_field_name |
str |
new_{model_name} |
Name of the mutation input argument |
output_field_name |
str |
{model_name} |
Name of the mutation output field |
results_field_name |
str |
'results' |
Name of the results field |
nested_fields |
dict |
() |
Nested field configuration: {accessor_name: ChildModel}. Anything that is not a dict is ignored |
filter_fields |
dict |
None |
Field filtering configuration |
description |
str |
Auto-generated | Type description |
stream |
str |
None |
Subscription stream name; required to expose a subscription field |
payload_mode |
str |
None |
Force "full" or "id_only" subscription payloads; None inherits the global setting |
subscription_index_fields |
tuple/list |
None |
Model field names used to route notifications to value-scoped subscriber groups |
max_depth |
int |
None |
Max nested-object depth below the generated output type (see Query depth limiting) |
complexity |
int |
None |
Cost weight of the generated output type (see Query cost analysis) |
model_operations |
tuple |
("create", "update", "delete", "list", "retrieve") |
The operations this type serves. Anything left out has its *Field() builder raise, and stops counting when a parent nests this model |
An automatic delete in id-only mode still publishes only the primary key. Its private channel-layer envelope may carry projected scalar values to decide plain equality and server scope after the row is gone; these values are not GraphQL fields or transport output. See Filtering notifications.
model_operations — declaring a read-only type
The default is every operation, so a type that says nothing behaves exactly as it always has. Narrowing it does two things:
- the
*Field()builder for an excluded operation raisesAttributeError, andQueryFields()/MutationFields()return only what is enabled — the same contractDjangoModelMutation.Meta.model_operationshas always had; - the type stops being a write host for the operations it dropped. When
another model nests this one through
Meta.nested_fields, a nested write is gated by every declared host of the child (see Nested writes). A display card declaringmodel_operations = ("list", "retrieve")therefore keeps itsMeta.querysetand itsonly_fieldsout of that write path, where they were never meant to be a policy.
class ProfileCard(DjangoModelType):
class Meta:
model = Profile
model_operations = ("list", "retrieve")
only_fields = ("id", "display_name", "avatar")
queryset = Profile.objects.filter(is_public=True)
>>> ProfileCard.CreateField()
AttributeError: "create" is not enabled on ProfileCard;
Meta.model_operations is ('list', 'retrieve').
The model here is Profile and not User for a reason the next warning
spells out: every User block on this page registers an output type of its
own, and only_fields on a DjangoModelType whose model already has one
raises. A read-only card is exactly the shape that trips it.
The projection needs an output type this type actually builds
A DjangoModelType reuses the output type already registered for its model
— a DjangoObjectType you declared for the same model — and that type was
built from its own Meta. Declaring only_fields, include_fields or
exclude_fields here in that situation raises ImproperlyConfigured at
class definition, naming the option, the model and the type that registered
the output type. Move the projection to that DjangoObjectType (or drop the
option); it is honored as usual when no other type registered the model.
Declared after the UserType at the top of this page,
the card above would have read:
ImproperlyConfigured: UserCard.Meta.only_fields cannot be honored: the
output type for User is reused from UserType, which was built from its own
Meta, so the projection would be silently dropped and any field it hides
would stay exposed. Declare only_fields on UserType instead, or remove the
option.
(Fixed in 2.2.0: 2.1.0 and earlier dropped the option silently, so a column excluded here stayed queryable.)
Generated Methods¶
QueryFields(**kwargs) (classmethod)¶
Generate both single object and list query fields.
Returns: Tuple of (single_field, list_field), restricted to the
operations enabled in Meta.model_operations
ListField(**kwargs) (classmethod)¶
Create a list field for this model type.
Returns: DjangoListObjectField instance
RetrieveField(**kwargs) (classmethod)¶
Create a retrieve field for single objects.
Returns: DjangoObjectField instance
MutationFields(**kwargs) (classmethod)¶
Generate the create, delete and update mutation fields.
Returns: Tuple of (create_field, delete_field, update_field), restricted
to the operations enabled in Meta.model_operations
CreateField(**kwargs) / DeleteField(**kwargs) / UpdateField(**kwargs) (classmethod)¶
Create the individual mutation fields wired to the create, delete and
update resolvers respectively.
Returns: Field instance
Override Hooks¶
DjangoModelType exposes hooks you can override on your subclass to scope
querysets and enforce authorization across all CRUD operations:
get_queryset(manager, info, **kwargs) (classmethod)¶
Return the base queryset for retrieve, list and mutation responses. The default
uses Meta.queryset (falling back to the model's default manager) and applies
filter_queryset. Override to add select_related/annotate, etc.
filter_queryset(qs, info, **kwargs) (classmethod)¶
Per-request scoping hook. The default returns qs unchanged.
The scope covers writes, not only reads
retrieve, list, update and delete all resolve their target rows
through get_queryset → filter_queryset. A row outside the scope answers
an update/delete exactly as a missing row does — ok: false with the
standard <Model> with id <pk> does not exist. error — so the response
cannot be used to probe which primary keys exist outside the scope.
(Fixed in 2.2.0: 2.1.0 and earlier resolved the write target from the bare
model, so a scope enforced on the read path left update and delete open
to any row in the table.)
authorize(info, action, **kwargs) (classmethod)¶
Authorization hook called by every CRUD method before it runs. The default
delegates to check_permissions using the configured permission_classes,
raising GraphQLError when denied. Override to customize.
permission_classes (class attribute)¶
Tuple of permission classes checked per action. Empty (the default) means no
checks. A permission denies on any falsy return value (False, None,
0, ""), so return user and user.is_staff is safe. See
django_graphex.permissions.
Example Usage¶
from django_graphex.core import ObjectType
from django_graphex.schema import DjangoGraphQLSchema
class Query(ObjectType):
# Generate both fields automatically
article, articles = ArticleType.QueryFields()
# Or create individual fields
article_list = ArticleType.ListField()
single_article = ArticleType.RetrieveField()
schema = DjangoGraphQLSchema(query=Query)
Type Registration¶
All types are automatically registered in a global registry for reuse and relationship resolution.
Registry Operations¶
from django_graphex.registry import get_global_registry
# Get the global registry
registry = get_global_registry()
# Check if a type is registered
user_type = registry.get_type_for_model(User)
# Register a type manually
registry.register(CustomUserType)
Registering the same class again is idempotent, and re-executing the same
declaration (reload, re-import) replaces the stale class. A user type also
replaces a library-generated placeholder. Registering a distinct public
DjangoObjectType for the same exact model in this registry (a subclass with
its own Meta.model included) raises ImproperlyConfigured before it can
replace the nested relation target; set skip_registry = True on the extra type
or use separate registries and schema pairs for intentionally different public views;
see one public output type per model and registry.
Types generated dynamically (for example inside a function or a loop) follow
one lifecycle rule. Re-defining a class with the same module and qualified name
replaces its previous compile entries, so repeating one declaration does not
grow the process-wide output list (100 redefinitions leave one entry). Each
distinct generated name keeps its entry for the life of the registry (100
distinct names leave 100 entries), because the library cannot know that a
differently named class is abandoned. Generate types once at import time, reuse
one name per logical type, or build the types of a short-lived schema against a
dedicated Registry.
Custom Registry¶
Registry is part of the public API and can be imported directly from
django_graphex:
from django_graphex.registry import Registry
# Create custom registry
custom_registry = Registry()
class UserType(DjangoObjectType):
class Meta:
model = User
registry = custom_registry
only_fields = ('id', 'username', 'first_name', 'last_name')
Advanced Usage¶
Custom Field Resolvers¶
from django_graphex.core import CharField, IntField
class UserType(DjangoObjectType):
full_name = CharField()
post_count = IntField()
class Meta:
model = User
only_fields = ('id', 'username', 'first_name', 'last_name')
def resolve_full_name(self, info):
return f"{self.first_name} {self.last_name}"
def resolve_post_count(self, info):
return self.posts.count()
Dynamic Field Generation¶
class UserType(DjangoObjectType):
class Meta:
model = User
only_fields = ('id', 'username', 'first_name', 'last_name')
@classmethod
def __init_subclass_with_meta__(cls, **options):
# Add dynamic fields before calling super
from django_graphex.core import CharField
cls.custom_field = CharField()
super().__init_subclass_with_meta__(**options)
Performance Optimization¶
class UserType(DjangoObjectType):
class Meta:
model = User
only_fields = ('id', 'username', 'first_name', 'last_name')
filter_fields = {
'username': ('exact', 'icontains'),
}
@classmethod
def get_queryset(cls, queryset, info):
# Optimize queries with select_related and prefetch_related
return queryset.select_related(
'profile'
).prefetch_related(
'posts',
'posts__comments'
)
Error Handling¶
Type Validation¶
class UserType(DjangoObjectType):
class Meta:
model = User
only_fields = ('id', 'username', 'first_name', 'last_name')
@classmethod
def is_type_of(cls, root, info):
# Custom type checking logic
if hasattr(root, 'user_type'):
return root.user_type == 'standard'
return super().is_type_of(root, info)
Field Resolution Errors¶
from django_graphex.core import CharField
class UserType(DjangoObjectType):
avatar_url = CharField()
class Meta:
model = User
only_fields = ('id', 'username', 'first_name', 'last_name')
def resolve_avatar_url(self, info):
try:
if self.profile and self.profile.avatar:
return self.profile.avatar.url
return None
except AttributeError:
return None
Best Practices¶
Type Best Practices
- Use Descriptive Names: Choose clear, descriptive type names
- Control Field Exposure: Use
only_fieldsorexclude_fieldsappropriately - Optimize Queries: Implement
get_querysetfor performance optimization - Handle Null Values: Always handle potential null values in resolvers
- Document Types: Provide meaningful descriptions for types and fields
- Separate Concerns: Use different input types for different operations
Security Considerations¶
class UserType(DjangoObjectType):
class Meta:
model = User
# Allow-list the complete public account surface.
only_fields = ('id', 'username', 'first_name', 'last_name')
@classmethod
def get_queryset(cls, queryset, info):
# Apply security filters
if not info.context.user.is_staff:
return queryset.filter(is_active=True)
return queryset
The projection reaches subscriptions too
only_fields / exclude_fields on a DjangoModelType (or directly on a
Subscription) also gate the generated subscription: an excluded column is
absent from the event type, from the broadcast payload, and — since 2.1.0 —
from the generated <Model>SubscriptionFilterInput that types the filter
argument. Since client filters run as ORM lookups at delivery time, that
projection is the supported way to keep a sensitive column off the
subscription surface — see
Subscriptions › Filter key validation.
That input type declares only exact, iexact, in and isnull per
field, so a declared column can be tested for equality but not walked
with startswith / gt / icontains — those do not exist in the schema.
The projection is still what keeps a column off the surface entirely.
The projection that gates it is the subscription's own. A hand-written
Subscription whose Meta.model names a model builds its event type and
its filter input from that model, not from whatever DjangoObjectType
the schema registered for it — so a column hidden on the node type is still
both selectable and filterable on that subscription unless you repeat the
projection in the subscription's Meta. DjangoModelType is what makes the
tip above true: it forwards its own only_fields / exclude_fields
into the subscription it generates, so declare the projection there and the
generated subscription inherits it. See
The one exception, and the open boundaries.
(Fixed in 2.1.0: 2.0.0 silently dropped the option on the subscription path, so the excluded column stayed both serialized and filterable. 2.1.0 moved the same boundary into the type system.)
DjangoUnionType¶
GraphQL Union over explicitly enumerated DjangoObjectType members.
Meta Options¶
| Option | Type | Default | Description |
|---|---|---|---|
types |
tuple[DjangoObjectType, ...] |
Required (≥ 1) | Member types of the union. Members are enumerated explicitly — the django_content_type table is never queried to discover them. |
Declaration Order (load-bearing)¶
- Declare all member
DjangoObjectTypes first. - Declare the
DjangoUnionTypewithMeta.types. - Declare the owner
DjangoObjectTypeLAST, referencing the union viaMeta.unions(renamed fromgfk_unionsin 2.0; the old key raisesImproperlyConfigured).
A mis-ordered declaration logs a WARNING and falls back to GenericForeignKeyType — the schema still builds.
Methods¶
resolve_type(instance, info) (classmethod)¶
Maps a Django model instance to its registered DjangoObjectType via the
global registry. Raises a descriptive TypeError if no type is registered
for the instance's model (rather than returning None, which would surface an
opaque GraphQL error).
You do not override this method.
Example Usage¶
from django_graphex.types import DjangoObjectType, DjangoUnionType
class AccountType(DjangoObjectType):
class Meta:
model = Account
class InvoiceType(DjangoObjectType):
class Meta:
model = Invoice
class CommentTargetUnion(DjangoUnionType):
class Meta:
types = (AccountType, InvoiceType)
class CommentType(DjangoObjectType):
class Meta:
model = Comment # has `target = GenericForeignKey(...)`
unions = {"target": CommentTargetUnion}
See Types — DjangoUnionType for the full usage guide including optimizer behavior.
When a GFK uses the flat GenericForeignKeyType fallback, a
permission-pruned schema
keeps the same SDL but returns the whole target as null without the actual
target model's read permission or when its registered row hook excludes it.
Unpruned schemas and explicit typed unions retain their previous behavior.
DjangoInterfaceType¶
GraphQL Interface for shared field declarations across multiple
DjangoObjectType implementors.
Meta Options¶
No additional Meta options beyond the GraphQL interface basics.
Implementors declare membership via the Meta.interfaces kwarg.
The name= parameter on a declared interface field is its exact GraphQL wire
name. Implementing types must expose the same name and compatible type;
the Python attribute still determines resolver lookup. With no explicit name,
the attribute is camel-cased. See the migration note: an old accidental field such
as internalName becomes its requested wire name, for example name, without
an automatic legacy alias.
Methods¶
resolve_type(instance, info) (classmethod)¶
Maps a Django model instance to its registered concrete DjangoObjectType implementor
via the global registry. Raises TypeError if the instance's model has no
registered type.
You do not override this method.
Example Usage¶
from django_graphex.core import CharField
from django_graphex.types import DjangoInterfaceType, DjangoObjectType
class ProductInterface(DjangoInterfaceType):
name = CharField()
class Meta:
pass
class BookType(DjangoObjectType):
class Meta:
model = Book
interfaces = (ProductInterface,)
class MagazineType(DjangoObjectType):
class Meta:
model = Magazine
interfaces = (ProductInterface,)
See Types — DjangoInterfaceType for the full usage guide.
This comprehensive API reference covers all type classes in django-graphex, providing developers with the knowledge needed to effectively create and customize GraphQL types for their Django applications.