Skip to main content

django-graphql

SDL-first GraphQL for Django. Write your API contract in plain GraphQL SDL, bind object types to Django models with a @model directive, and let @queryset / @get generate resolvers that filter, page and join efficiently. No DjangoObjectType classes, no Python mirror of your schema: the .graphql text is the schema.

Because the SDL and the models are separate sources of truth, django-graphql ships a drift checker that reports any SDL field or filter argument your models no longer back, and can plug it into manage.py check.

Features

  • Schema-first: build an executable schema from an SDL string with Schema(sdl).
  • Model binding: type Book @model(name: "library.Book") maps a type to a model; fields resolve by attribute, with automatic camelCase to snake_case fallback (publishedYear reads published_year). Properties and methods work too.
  • Generated resolvers:
    • @queryset on a list field: every argument becomes an ORM lookup (title__icontains, publishedYear__gte, author__name). limit, offset and orderBy are reserved for paging and ordering. Null arguments are ignored.
    • @get on a single-object field: arguments become a .get() lookup. Returns null when nothing matches.
  • N+1 avoidance: the selection set is inspected, then forward FKs and one-to-ones are select_related while reverse FKs and many-to-many are prefetch_related (one level deep, fragments included).
  • Safe paging: max_limit caps every @queryset result (default 100).
  • Custom resolvers: register them with @schema.resolver("Type.field").
  • Drift checks: schema.check() returns a list of SchemaIssues, and register_django_check(schema) reports them through Django's system checks.
  • Plain Django view: GET and POST, JSON or form-encoded, CSRF-exempt. Mutations over GET are refused, and info.context is the HttpRequest.

Install

pip install django-graphql

Requires Python 3.10+, Django 4.2+ and graphql-core 3.2/3.3. Adding the package to INSTALLED_APPS is not required.

Quickstart

# library/models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    published_year = models.IntegerField()
    author = models.ForeignKey(Author, related_name="books", on_delete=models.CASCADE)
# library/schema.py
from django_graphql import Schema, register_django_check

schema = Schema("""
type Author @model(name: "library.Author") {
  id: ID!
  name: String!
  books: [Book!]!
}

type Book @model(name: "library.Book") {
  id: ID!
  title: String!
  publishedYear: Int!
  author: Author!
}

type Query {
  books(title__icontains: String, publishedYear__gte: Int,
        limit: Int, offset: Int, orderBy: [String!]): [Book!]! @queryset
  book(id: ID!): Book @get
  me: String
}

type Mutation {
  renameAuthor(id: ID!, name: String!): Author
}
""", max_limit=50)

@schema.resolver("Query.me")
def resolve_me(root, info):
    return info.context.user.get_username()   # info.context is the HttpRequest

@schema.resolver("Mutation.renameAuthor")
def rename_author(root, info, id, name):
    from library.models import Author
    author = Author.objects.get(pk=id)
    author.name = name
    author.save()
    return author

register_django_check(schema)   # `manage.py check` now reports SDL/model drift
# urls.py
from django.urls import path
from library.schema import schema

urlpatterns = [path("graphql/", schema.as_view())]
{
  books(publishedYear__gte: 1970, orderBy: ["-publishedYear"], limit: 10) {
    title
    author { name }      # select_related("author"): one SQL query total
  }
}

You can also execute without HTTP:

result = schema.execute("query($id: ID!) { book(id: $id) { title } }", {"id": "1"})
result.data, result.errors

API overview

All names below are importable from django_graphql.

SDL directives (definitions are injected automatically)

Directive Location Meaning
@model(name: String!) object type Bind the type to the model "app_label.ModelName".
@queryset field Return type must be a list of a @model type. Non-reserved arguments become QuerySet.filter(**lookups) (names converted to snake_case). Reserved: limit, offset, orderBy ([String!], - prefix for descending).
@get field Return type must be a single @model type. All arguments become a .get() lookup. Returns null on no match and an error if several rows match.

Schema(sdl, *, resolvers=None, max_limit=100)

Builds the executable schema. Raises SchemaError for an unknown model, a misplaced directive (@queryset on a non-list, @get on a list, either on a non-@model type, or both on one field) or an unknown resolver path.

  • resolvers: optional {"Type.field": callable} mapping, the same as applying resolver() to each.
  • max_limit: cap (and default) for limit on every @queryset field. None means no cap.

Attributes:

  • sdl: str: the SDL you passed in.
  • max_limit: int | None
  • graphql_schema: graphql.GraphQLSchema: the underlying graphql-core schema.
  • models: dict[str, type[Model]]: type name to bound model.
  • bound_fields: dict[str, tuple[type[Model], "queryset" | "get"]]: "Type.field" to its generated binding.
  • custom_resolvers: dict[str, Callable]: "Type.field" to the registered resolver.

Methods:

  • resolver(path: str): decorator registering func(source, info, **args) for "Type.field". It replaces a generated resolver, evaluates returned querysets and managers to lists, and returns the original function.
  • execute(query, variables=None, *, operation_name=None, context=None, root=None) -> graphql.ExecutionResult: runs the query synchronously.
  • check() -> list[SchemaIssue]: reports drift (see below).
  • as_view(**initkwargs): a CSRF-exempt Django view (GraphQLView) for this schema.

SchemaError(Exception)

Raised when the SDL cannot be bound to Django models.

SchemaIssue(path: str, message: str)

A frozen dataclass. path is "Type.field" or "Type.field(arg)", and str(issue) gives "path: message". schema.check() reports:

  • a field on a @model type that is not a model field, reverse accessor, property or method (camelCase is also tried as snake_case). Fields with a custom resolver are skipped.
  • a @queryset / @get argument whose first lookup segment (publisher in publisher__name) is not a model field or pk.

register_django_check(schema, *, check_id="django_graphql.E001")

Registers a system check (tag django_graphql) that turns each SchemaIssue into a checks.Error with obj=issue.path.

GraphQLView

A Django View with a class attribute schema. Usually you create it with schema.as_view(), or GraphQLView.as_view(schema=schema).

  • POST accepts application/json {"query", "variables", "operationName"} or form fields with the same names (variables as a JSON string).
  • GET accepts ?query=&variables=&operationName=, for queries only. A mutation gets 405 with Allow: POST.
  • The response is {"data": ..., "errors": [...]}. A malformed request, a syntax error or a validation error returns 400. An execution error returns 200 with partial data, or 400 if data is entirely null.
  • info.context is the HttpRequest.

default_field_resolver(source, info, **args)

The resolver used for every field without a generated or custom resolver. It reads source.<name> or source.<snake_name> (or the dict keys of the same names), calls callables with the field arguments, and evaluates managers and querysets to lists.

Development

python3 -m venv .venv
.venv/bin/pip install -e ".[test]"
.venv/bin/python -m pytest

The tests configure Django in tests/conftest.py (in-memory SQLite, no settings module).

License

MIT

Metadata

Release files for django-graphql 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-graphql 0.1.0
File Size Uploaded Provenance
django_graphql-0.1.0.tar.gz 21.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for django-graphql 0.1.0
File Interpreter ABI Platform
django_graphql-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 37.9 kB

Release files / django_graphql-0.1.0.tar.gz

Download URL django_graphql-0.1.0.tar.gz
Size 21.0 kB
Tags Source
SHA-256 checksum
How to use checksums
d945718399d40cb7ba830e697746af79ec069f3407eda4eec3065749aed1e7d9
BLAKE2b-256 checksum
How to use checksums
71d2cc1f741bdcf5e911fe5fe5d20d8406f1113a5ecebfb0f2a81a44b9afe870
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / django_graphql-0.1.0-py3-none-any.whl

Download URL django_graphql-0.1.0-py3-none-any.whl
Size 17.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d4903ecb3478fc4752a8b9be21c64d60c50f0114cb376ee0095e832ba41de7ac
BLAKE2b-256 checksum
How to use checksums
ca8fbabda1b8f52c8894457dc17ec234f56724fe447d8be529afa6fd23c7172c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for django-graphql 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

0.0.1

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page