Mutations API Reference¶
This section provides detailed API documentation for mutation classes in django-graphex.
DjangoModelMutation¶
The primary mutation class that provides automatic CRUD operations driven directly by a Django model.
Meta Configuration¶
Configure mutations through a nested Meta class:
class CustomerMutation(DjangoModelMutation):
class Meta:
model = Customer
description = "Customer CRUD operations"
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 |
only_fields |
tuple/list |
() |
Include only specified fields |
exclude_fields |
tuple/list |
() |
Exclude specified fields |
include_fields |
tuple/list |
() |
Additional fields to include |
input_field_name |
str |
'new_{model}' |
Name of input argument |
output_field_name |
str |
'{model}' |
Name of output field |
description |
str |
Auto-generated | Mutation description |
nested_fields |
dict |
() |
Nested field configuration — a {field_name: Model} mapping. The empty default means no nested writes. Every key must name a relation accessor on model; one that does not raises ImproperlyConfigured, listing the accessors that would have worked. |
model_operations |
tuple |
("create", "update", "delete") |
Which CRUD operations to generate; any subset of ("create", "update", "delete"). Calling the *Field() builder for an excluded operation raises AttributeError. |
registry |
Registry |
Global registry | Type registry the mutation's output node and input type resolve against. A custom registry scopes the whole mutation subgraph to one schema's own pair, so a forked DjangoGraphQLSchema re-forks the payload's output node into its own namespace instead of reaching the process-global node. |
Anything outside this table raises
Every Meta key django-graphex itself reads is in the table above; any other
key raises ImproperlyConfigured at class definition. That covers the typo
(exclude_field for exclude_fields, which used to leave the column it
named writable) and the option that belongs to the other host: queryset is
read by DjangoModelType, never here. Declaring permission_classes raises
for the same reason — see
Mutations.
The one exception is the handful of keys the graphene ObjectType base
consumes before django-graphex sees them — name, _meta, interfaces,
possible_types, default_resolver and container. They are accepted, and
only name and description do anything useful on a mutation.
Fields¶
Every DjangoModelMutation includes these standard fields:
| Field | Type | Description |
|---|---|---|
ok |
Boolean |
Success indicator |
errors |
List[ErrorType] |
Validation errors |
{model_name} |
ObjectType |
The created/updated/deleted object |
Class Methods¶
__init_subclass_with_meta__(**kwargs) (classmethod)¶
Initialize the mutation subclass with meta configuration.
Parameters:
- model (Model): Required Django model class
- pydantic_model (BaseModel): Optional Pydantic model for custom validation
- only_fields (tuple): Fields to include
- exclude_fields (tuple): Fields to exclude
- include_fields (tuple): Additional fields
- input_field_name (str): Input argument name
- output_field_name (str): Output field name
- description (str): Mutation description
- nested_fields (dict): Nested field configuration
- model_operations (tuple): CRUD operations to generate
- registry (Registry): Type registry the output node and input type resolve against
get_errors(errors) (classmethod)¶
Create error response with provided errors.
Parameters:
- errors (list): List of error objects
Returns: Mutation instance with errors
perform_mutate(obj, info) (classmethod)¶
Create successful mutation response.
Parameters:
- obj (Model): The model instance
- info (ResolveInfo): GraphQL resolve info
Returns: Mutation instance with success response
save_with_nested(root, info, data, instance=None, serializer_kwargs=None) (classmethod)¶
Validate and persist the parent plus any Meta.nested_fields children
atomically (provided by NestedFieldsMixin). Forward FK/O2O children are
written before the parent and their pk injected; reverse FK/O2O and M2M children
are written after and linked to it. A reverse child (FK or O2O) supplied by
pk is rejected when it currently belongs to a different parent. Any validation
failure rolls the whole transaction back.
See How nested writes work.
Parameters:
- root (Any): Root object
- info (ResolveInfo): GraphQL resolve info
- data (dict): Input data (nested entries are popped from it)
- instance (Model | None): Existing instance for an update, else None
- serializer_kwargs (dict | None): Reserved (unused by the native backend)
Returns: (ok: bool, obj_or_errors) — the saved object, or a list of ErrorType
CRUD Operations¶
create(root, info, **kwargs) (classmethod)¶
Create a new object using the provided data.
Parameters:
- root (Any): Root object
- info (ResolveInfo): GraphQL resolve info
- **kwargs: Mutation arguments including input data
Returns: Mutation response with created object or errors
update(root, info, **kwargs) (classmethod)¶
Update an existing object with provided data.
Parameters:
- root (Any): Root object
- info (ResolveInfo): GraphQL resolve info
- **kwargs: Mutation arguments including input data
Returns: Mutation response with updated object or errors
delete(root, info, **kwargs) (classmethod)¶
Delete an object by its ID.
Parameters:
- root (Any): Root object
- info (ResolveInfo): GraphQL resolve info
- **kwargs: Mutation arguments including object ID
Returns: Mutation response with deleted object or errors
Customizing persistence
There is no separate save hook. To run logic around create/update, override
create / update and call super(); to change how the parent and its
nested children are validated and written, override save_with_nested.
Field Generation Methods¶
CreateField(*args, **kwargs) (classmethod)¶
Create a GraphQL field for create mutations.
Returns: Field instance configured for create operations
UpdateField(*args, **kwargs) (classmethod)¶
Create a GraphQL field for update mutations.
Returns: Field instance configured for update operations
DeleteField(*args, **kwargs) (classmethod)¶
Create a GraphQL field for delete mutations.
Returns: Field instance configured for delete operations
MutationFields(*args, **kwargs) (classmethod)¶
Get all mutation fields (create, delete, update).
Returns: Tuple of (create_field, delete_field, update_field)
Example Usage¶
from django_graphex.core import BooleanField
class CustomerMutation(DjangoModelMutation):
class Meta:
model = Customer
class Arguments:
send_email = BooleanField(
default=False,
description="Send welcome email",
)
@classmethod
def create(cls, root, info, **kwargs):
send_email = kwargs.pop('send_email', False)
response = super().create(root, info, **kwargs)
if response.ok and send_email:
send_welcome_email(getattr(response, cls._meta.output_field_name).email)
return response
from pydantic import BaseModel, field_validator
class CustomerValidation(BaseModel):
@field_validator("email", check_fields=False)
@classmethod
def corporate_only(cls, value):
if value and not value.endswith("@example.com"):
raise ValueError("Only corporate email addresses are accepted.")
return value
class CustomerMutation(DjangoModelMutation):
class Meta:
model = Customer
# supply a Pydantic model with extra validators; the derived
# model fields extend it
pydantic_model = CustomerValidation
Schema Integration¶
from django_graphex.core import ObjectType
from django_graphex.schema import DjangoGraphQLSchema
class Mutation(ObjectType):
create_customer = CustomerMutation.CreateField()
update_customer = CustomerMutation.UpdateField()
delete_customer = CustomerMutation.DeleteField()
schema = DjangoGraphQLSchema(query=Query, mutation=Mutation)
GraphQL Operations¶
Create Mutation¶
mutation CreateCustomer($customerData: CustomerInput!) {
createCustomer(newCustomer: $customerData) {
ok
customer {
id
username
email
}
errors {
field
messages
}
}
}
Update Mutation¶
mutation UpdateCustomer($customerData: CustomerInput!) {
updateCustomer(newCustomer: $customerData) {
ok
customer {
id
username
email
}
errors {
field
messages
}
}
}
Delete Mutation¶
mutation DeleteUser($id: ID!) {
deleteCustomer(id: $id) {
ok
customer {
id
username
}
errors {
field
messages
}
}
}
Response Structure¶
Success Response¶
{
"data": {
"createCustomer": {
"ok": true,
"customer": {
"id": "1",
"username": "john_doe",
"email": "john@example.com"
},
"errors": null
}
}
}
Error Response¶
{
"data": {
"createCustomer": {
"ok": false,
"customer": null,
"errors": [
{
"field": "username",
"messages": ["This field is required."]
},
{
"field": "email",
"messages": ["Enter a valid email address."]
}
]
}
}
}
Internal options container
DjangoModelMutation._meta is a NativeObjectTypeOptions instance
(django_graphex.core.base.NativeObjectTypeOptions). It is an internal
detail — the public API is the Meta class options documented above.
Advanced Usage¶
File Upload Support¶
When the request content type is multipart/form-data, an uploaded part is
merged into the input payload when its name matches a file field the
mutation's own input publishes — under either spelling, the camelCase wire
name or the model's own attribute name. A part matching nothing is ignored
rather than saved, and a column the input projects away is not writable through
a part any more than it is through the JSON body: the projection is the same
boundary on both. Matching parts are saved on create and on update:
class ProfileMutation(DjangoModelMutation):
class Meta:
model = Profile # model has an ImageField named "avatar"
# A multipart part named "avatar" lands on Profile.avatar.
mutation UpdateProfile($profileData: ProfileInput!) {
updateProfile(newProfile: $profileData) {
ok
profile {
id
avatar # reads back as the storage path (String)
bio
}
errors {
field
messages
}
}
}
The GraphQL input field stays String: the file travels in the multipart body,
never in the GraphQL variables. That field also accepts a plain storage path
string, and rejects any other shape with a structured error.
The request carries a query part with the document, an optional
variables part with its variables as JSON, and one part per file named
after the model field — the view reads a multipart body straight out of
request.POST, so there is no operations / map envelope:
requests.post(
"https://app.example.com/graphql/",
files={"avatar": open("avatar.png", "rb")},
data={
"query": "mutation UpdateProfile($profileData: ProfileInput!) { … }",
"variables": json.dumps({"profileData": {"bio": "hi"}}),
},
headers={"X-Requested-With": "XMLHttpRequest"}, # <- required
)
A multipart POST must carry X-Requested-With
multipart/form-data is a CORS-simple content type, so a browser posts it
cross-site with no preflight and the csrf_exempt endpoint would otherwise
execute a forged <form> submit under the victim's session cookie. A
multipart request without the header is refused with HTTP 403 before its
body is read. See
Security → Cross-site POST protection
for the REQUIRE_CSRF_HEADER opt-out.
Top-level fields only
The merge is keyed by the bare form-field name, so a file field on a child
declared in Meta.nested_fields cannot be addressed — and naming a part
after the relation itself overwrites the nested payload and raises an
uncaught ValueError (an HTTP 500). For nested uploads, use the base64
input described in Mutations.
Authentication & Authorization¶
from graphql import GraphQLError
class CustomerMutation(DjangoModelMutation):
class Meta:
model = Customer
@classmethod
def create(cls, root, info, **kwargs):
user = info.context.user
if not user.is_authenticated:
raise GraphQLError("Authentication required")
if not user.has_perm('crm.add_customer'):
raise GraphQLError("Permission denied")
return super().create(root, info, **kwargs)
Custom Error Handling¶
from django.core.exceptions import ValidationError
from django_graphex.errors import ErrorType
class CustomerMutation(DjangoModelMutation):
class Meta:
model = Customer
@classmethod
def create(cls, root, info, **kwargs):
try:
return super().create(root, info, **kwargs)
except ValidationError as e:
return cls.get_errors([
ErrorType(field=field, messages=messages)
for field, messages in e.message_dict.items()
])
Error Types¶
ErrorType¶
Standard error type used in mutation responses.
ErrorType is a native ObjectType (graphene-free) with two fields:
| Field | GraphQL type | Description |
|---|---|---|
field |
String! |
The name of the field that failed validation |
messages |
[String!]! |
One or more error messages for that field |
Best Practices¶
Mutation Best Practices
- Leverage Pydantic Validation: Use
Meta.pydantic_modelto add custom validators - Handle Permissions: Always check authentication and authorization
- Validate Input: Rely on the auto-generated or custom Pydantic model for robust input handling
- Return Meaningful Errors: Provide clear, actionable error messages
- Test Thoroughly: Test all CRUD operations and edge cases
- Document Operations: Provide clear descriptions for mutations
- Handle Files: Use proper file upload handling for media fields
Security Considerations¶
class CustomerMutation(DjangoModelMutation):
class Meta:
model = Customer
# Don't expose server-owned business fields.
exclude_fields = ('internal_notes', 'credit_limit')
@classmethod
def create(cls, root, info, **kwargs):
if not info.context.user.has_perm('crm.add_customer'):
raise GraphQLError("Permission denied")
return super().create(root, info, **kwargs)
This comprehensive API reference covers the mutation system in django-graphex, providing developers with the tools needed to create robust, validated GraphQL mutations for their Django applications.