Skip to main content
DRF And React By Schema - Django

drf-react-by-schema-django is the Django/DRF half of the DRF and React By Schema suite. Given a Django app’s models, it generates the full DRF API surface (serializers, viewsets and routes) and a machine-readable schema that the companion React package (drf-react-by-schema on npm) consumes to build forms, tables and validation automatically.

Overview

Django and DRF are a solid foundation for defining data models and exposing them over an API, but building the matching React UI (form widgets, validation, autocompletes, related-object editors) duplicates information that already lives in the models. This package removes that duplication:

  • It generates one DRF Serializer and one ViewSet per model in the configured apps, plus the routes (/api/<model>/, nested related routes and autocompletes).

  • It describes each model through DRF OPTIONS and exposes a machine-readable schema at GET /drbs/schema/.

  • The companion React package (drf-react-by-schema on npm) consumes those schemas to build the forms, grids and validation, while still allowing full layout customization.

  • The sync_react management command writes the schema and the TypeScript types into the React project.

The suite is published for React at https://www.npmjs.com/package/drf-react-by-schema and for Django at https://pypi.org/project/drf-react-by-schema/.

Requirements

Installed automatically with the package (see setup.cfg):

  • Django 3.2 - 5.2

  • djangorestframework

  • djangorestframework-simplejwt

  • drf-nested-routers

  • django-filter

  • django-cors-headers

  • drf-excel

The package itself imports with no PostgreSQL dependency. ArrayField (Django’s PostgreSQL array field) additionally needs a PostgreSQL driver (psycopg2 or psycopg) to be importable; it is resolved lazily, so a project that does not use it needs no driver.

Compatibility

This package supports Django 3.2 through 5.2 and Python 3.10+ from a single release line. The supported combinations are exercised in CI (see .gitlab-ci.yml / tox.ini). install_requires caps Django at <6.0 so a fresh install cannot pull an untested major; the cap is raised when the supported range is extended.

Django

Python

Notes

3.2

3.10

End of upstream life. Pin drf-excel<2.6 (see below).

4.2

3.10 - 3.12

LTS

5.0

3.10 - 3.12

5.1

3.10 - 3.13

5.2

3.10 - 3.13

Django 3.2 is kept as legacy support: the 3.2-only pin above and its CI job will be dropped when the supported floor moves to 4.2. New code targets 4.2-5.2; Django-3.2-only branches stay isolated so the migration is a single clean-up pass.

Installation and quick start

  1. Install the package:

    pip install drf-react-by-schema
  2. Add drf_react_by_schema (and the pre-requisite apps) to your INSTALLED_APPS setting:

    INSTALLED_APPS = [
        ...
        'drf_react_by_schema',
    ]
  3. Configure Django Rest Framework with the package renderer, metadata and filter backend:

    REST_FRAMEWORK = {
        'DEFAULT_PERMISSION_CLASSES': (
            'rest_framework.permissions.IsAuthenticated',
        ),
        'DEFAULT_AUTHENTICATION_CLASSES': (
            'rest_framework_simplejwt.authentication.JWTAuthentication',
            'rest_framework.authentication.SessionAuthentication',
        ),
        "DEFAULT_RENDERER_CLASSES": [
            "rest_framework.renderers.JSONRenderer",
            "drf_react_by_schema.renderers.CustomBrowsableAPIRenderer",
        ],
        'DEFAULT_METADATA_CLASS': 'drf_react_by_schema.metadata.Metadata',
        'DEFAULT_FILTER_BACKENDS': [
            'rest_framework.filters.SearchFilter',
            'drf_react_by_schema.filters.DRFReactBySchemaOrderingFilter',
        ],
    }
  4. Configure DRF_REACT_BY_SCHEMA. The only required entry is the list of apps whose models should get an endpoint (default ['main']). Each entry is an app label ('main') or a dotted module path ('my_package.produtos') — the dotted form lets a distribution ship its Django app nested inside its own package instead of taking a top-level name:

    DRF_REACT_BY_SCHEMA = {
        'APPS': [
            'main',
        ],
        'PAGINATION_MODE': 'server',
        'PAGINATION_PAGE_SIZE': 50,
        'PAGINATION_PAGE_SIZE_OPTIONS': [5, 10, 25, 50, 100],
        'PAGINATION_MAX_PAGE_SIZE': 10000,
        'PAGINATION_MODE_NESTED_VIEWSETS': 'server',
        'PERMISSIONS': 'rest_framework.permissions.IsAuthenticated',
        'REACT_APP_DIR': 'frontend',
        # App-owned files written by sync_react next to the schemas, so a single
        # definition can feed both the Django runtime and the React app.
        'EXTRA_ARTIFACTS': [
            'main.my_single_source_of_truth.sync_artifact',
        ],
        # Expose GET /drbs/schema/ for MCP clients (default: on).
        'SCHEMA_ENDPOINT': True,
        # Fail (400) on a grid filter the endpoint cannot resolve, instead of
        # silently ignoring it (default: off, to tolerate a stale React layout).
        'GRID_FILTER_STRICT': False,
    }

    The full list of keys is in Configuration reference below.

  5. Include the package URL configuration in your project urls.py:

    urlpatterns = [
        ...
        path('', include('drf_react_by_schema.urls')),
    ]
  6. Apply the migrations. The package ships the Upload staging model used by the upload-first file endpoint (see Upload-first file staging):

    python manage.py migrate
  7. Generate the React schemas and TypeScript types (see Generating the React client (sync_react)):

    python manage.py sync_react
  8. Start the development server and open http://127.0.0.1:8000/api/endpoints to list every generated endpoint.

  9. Install the React package in your React app and follow its guide: https://www.npmjs.com/package/drf-react-by-schema.

Configuration reference

DRF_REACT_BY_SCHEMA is the only settings block the package reads (via settings.get("DRF_REACT_BY_SCHEMA", {})):

  • APPS (default ["main"]) — the apps whose models get generated serializers, viewsets and routes. Each entry is an app label ("main") or a dotted module path ("my_package.produtos"); the dotted form lets a distribution ship its Django app nested inside its own package instead of occupying a top-level name. utils.resolve_apps() normalizes an entry to (label, module_path) — the label keys apps.all_models and matches Model._meta.app_label, while the module path is imported as <module>.viewsets / <module>.serializers. Consumed by urls.py, viewsets.py, serializers.py, the autocomplete modules and sync_react.

  • REACT_APP_DIR (default "frontend") — where sync_react writes src/drfReactBySchema/, resolved as os.path.join(settings.BASE_DIR, REACT_APP_DIR).

  • EXTRA_ARTIFACTS (default []) — app-owned generated files, written by sync_react next to the schemas. A list of zero-argument callables (given as dotted paths or as the callables themselves); each returns a mapping of relative_path -> content. Use it to feed an app-owned single source of truth to both the Django runtime and the React client without duplicating it. See Custom generated artifacts (EXTRA_ARTIFACTS).

  • PERMISSIONS — the permission source for the generated viewsets (and for /drbs/schema/ and /drbs/upload/). A dotted-path string, a list/tuple of paths and/or classes, or a permission class. Default rest_framework.permissions.IsAuthenticated.

  • PAGINATION_MODE / PAGINATION_MODE_NESTED_VIEWSETS — "server" (or unset) toggles DRFReactBySchemaPagination on the top-level and nested viewsets respectively. See Pagination.

  • PAGINATION_PAGE_SIZE (default 50), PAGINATION_PAGE_SIZE_OPTIONS (default [5, 10, 25, 50, 100]) and PAGINATION_MAX_PAGE_SIZE (default 10000). The deprecated PAGINATION_PER_PAGE is still read as a fallback (with a DeprecationWarning).

  • FILTER_BY_CALLBACK / FILTER_LIST_BY_CALLBACK — global fallback callbacks for row-scoping. See Global permission-aware queryset filtering.

  • PASSWORD_RESET_CALLBACK — the delivery callback for the password-reset flow. See Account, password change and reset.

  • SCHEMA_ENDPOINT (default True) — whether GET /drbs/schema/ is registered.

  • ABOUT_PATH — path to the project ABOUT.md folded into the schema bundle (default <BASE_DIR>/ABOUT.md). See Machine-readable schema (GET /drbs/schema/).

  • GRID_FILTER_STRICT (default False) — whether an unresolvable grid filter raises a 400 instead of being ignored. See Grid filters.

Generating the React client (sync_react)

python manage.py sync_react regenerates the React client surface from the current models, serializers and viewsets. Run it after any model, serializer, viewset or field change. It deletes and recreates <REACT_APP_DIR>/src/drfReactBySchema/ and writes:

  • schemas/<Model>Schema.json — the OPTIONS POST schema for each model ({schema, verbose_name, verbose_name_plural});

  • types.ts — the TypeScript interfaces and enums;

  • optionsGetters.ts — get<Enum>Label helpers;

  • index.ts — the SchemaLoader exports consumed by the React provider;

  • pagination.ts — the resolved paginationConfig (perPage/pageSizeOptions/maxPageSize);

  • any files declared by DRF_REACT_BY_SCHEMA['EXTRA_ARTIFACTS'] (see Custom generated artifacts (EXTRA_ARTIFACTS)).

Every generated file is header-commented // @generated by drf-react-by-schema and /* eslint-disable */; do not edit them by hand.

The generated JSON/TS is the contract the React package consumes. Field type values, model_default, required, read_only, choices, conditional_visible, related_editable, validators_regex, date_views, decimal_places, model_multiline, max_file_size, geometry types and the inline-formset keys all feed the React widgets. Model class docstrings, field description= values and ABOUT.md are deliberately kept out of these files; they ride only in the schema bundle (see Machine-readable schema (GET /drbs/schema/)).

Custom generated artifacts (EXTRA_ARTIFACTS)

sync_react already derives the React client surface from your Django settings (the resolved pagination is the built-in example). EXTRA_ARTIFACTS extends that: your project can publish its own generated files with the same command, so one definition can be the source of truth for both sides:

  • the Django runtime imports the definition directly (it is ordinary Python module code) — e.g. to validate a payload, map a field to a section, or drive a serializer;

  • the React app imports the generated file from src/drfReactBySchema/, with no schema round-trip and no hand-maintained copy.

Reach for this whenever a structure is meaningful to both sides — a form/wizard layout, a shared enum or label map, feature flags, a pricing table, … The alternative (re-declaring it in TypeScript) drifts.

Configuration

DRF_REACT_BY_SCHEMA['EXTRA_ARTIFACTS'] is a list; each entry is a dotted path to a zero-argument callable, or the callable itself:

# myapp/settings.py
DRF_REACT_BY_SCHEMA = {
    'APPS': [...],
    'EXTRA_ARTIFACTS': ['myapp.wizard.sync_artifact'],
}

Each provider returns a mapping of relative_path -> content:

# myapp/wizard.py — one definition, used by Django *and* emitted to React
WIZARD = [
    {'title': 'Identificação', 'sections': [{'rows': ['nome', 'idade']}]},
    {'title': 'Produção', 'sections': [{'rows': ['produtos']}]},
]

def field_pages():
    """Map each field to the page that owns it (used by the Django side too)."""
    return {
        row: page
        for page in WIZARD
        for section in page['sections']
        for row in section['rows']
    }

def sync_artifact():
    return {'wizard.json': {'pages': WIZARD}}

A str content is written verbatim (use it for TypeScript sources); anything else is dumped as JSON (indent=2, ensure_ascii=False). Missing parent directories are created, so 'layouts/wizard.json' works too. On the React side:

import wizard from '@/drfReactBySchema/wizard.json';
// or a normal named import when the provider emits a .ts file.

Notes and good practices

  • Providers run after the built-in artifacts and after <REACT_APP_DIR>/src/drfReactBySchema/ has been recreated, so a provider may safely (re)write a file the command would otherwise wipe.

  • The package does not prepend a generated header to extra artifacts — include your own // @generated (or # @generated) marker if the file could be mistaken for hand-written.

  • Keep providers deterministic and side-effect free: the output is committed and diffed, so a value that changes on every run (a timestamp, a random id) only creates noise. Derive from code/settings, not from the database.

  • A provider whose dotted path does not import — or that raises — fails sync_react: the error surfaces instead of silently leaving a file out.

  • The setting is reported in the MCP schema bundle (system.drf_react_by_schema), so an assistant can see which artifacts a project publishes.

Overriding generated serializers and viewsets

For every model in DRF_REACT_BY_SCHEMA['APPS'] the package generates a <Model>Serializer and a <Model>ViewSet (plus one nested <Parent>Related<Child> viewset per reverse foreign key and an autocomplete viewset). A plain Django model therefore gets a full /api/<model>/ CRUD with no app-side code.

To customize one, define a class with the same name in the app’s serializers.py or viewsets.py. The app’s modules are registered before the package’s, so the app’s class wins the route and the viewset’s __init__ swaps in the app’s serializer by name:

# myapp/viewsets.py
from drf_react_by_schema.viewsets import GenericViewSet

class ProdutoViewSet(GenericViewSet):
    select_related = ["categoria"]
    filter_by_callback = filter_visible_products

The same first-seen-wins rule applies to nested and autocomplete routes, and to the MyAccountViewSet / MyAccountSerializer pair.

List vs. detail serializers

The generated XSerializer is the canonical serializer and serves both retrieve and list/excel. When a detail-only field would be expensive in lists, declare it in Meta.list_exclude on the app’s general serializer:

from drf_react_by_schema.serializers import (
    ClienteSerializer as BaseClienteSerializer,
)

class ClienteSerializer(BaseClienteSerializer):
    contratacao = drf_serializers.SerializerMethodField()   # detail only
    nome = drf_serializers.CharField(read_only=True)        # both

    class Meta(BaseClienteSerializer.Meta):
        list_exclude = ("contratacao",)

The package derives (and caches) a list projection that keeps every other field, including future additions to the general serializer. list_exclude takes serializer field names, not model source paths; () means “the list is identical to the detail response”. An explicit list serializer remains available as an escape hatch:

class ClienteViewSet(GenericViewSet):
    serializer_class = ClienteSerializer
    serializer_list_class = ClienteListSerializer   # optional, full override

XListSerializer classes are not generated automatically: the default is the general serializer, so app-added fields can never silently disappear from lists.

Custom model fields

The package ships drop-in replacements for the Django model fields that carry the extra metadata the React widgets need. Import them from drf_react_by_schema and use them in your models whenever a matching type exists: they behave like their Django counterparts at the database/migration level, and the extra keyword arguments are read by metadata.py into the OPTIONS schema. Using a plain django.db.models field drops that metadata and degrades the auto-built UI.

Available fields: CharField, TextField, EmailField, DecimalField, IntegerField, BooleanField, DateField, DateTimeField, FileField, ImageField, ForeignKey, ManyToManyField, OneToOneField, ArrayField, the PostGIS geometry fields (PointField, PolygonField, MultiPointField, MultiPolygonField, GeometryCollectionField) and the serializer fields TypedSerializerMethodField and NestedSerializerMethodField.

Metadata keywords (all read into the schema; all have a visible effect in the React frontend):

  • conditional_visible — show the field only while a condition on other fields holds (and clear its value on save while hidden). Accepts a single [source_field, source_value] pair, a list of pairs (OR’d), or a nested {"operator": "AND"|"OR", "conditions": [...]} expression for explicit logic:

    conditional_visible=("type", "book")
    conditional_visible=[("type", "book"), ("format", "paperback")]
    conditional_visible={
        "operator": "AND",
        "conditions": [
            ("type", "book"),
            {"operator": "OR", "conditions": [("format", "hardcover"), ("format", "paperback")]},
        ],
    }
  • ui_required — makes a DB-level optional (blank=True) field required in the UI, while it is shown.

  • related_editable (default True) — on relational fields; with True the user may create the related record inline from the form’s autocomplete, with False the relation is pick-only (still updatable to an existing record).

  • rich_text (TextField) — the column stores HTML; the React form renders a WYSIWYG editor and the detail view renders sanitized HTML.

  • pattern_format (CharField / IntegerField) — an input mask in the form and the grid. Named patterns: "cpf", "cnpj", "cep", and "telefone"/"fone"/"phone"/"contact"/"contato"; any other string is used as a literal mask where each # is one digit. On a CharField the mask also sets max_length to its digit count and adds a matching server-side RegexValidator; an IntegerField uses the mask for the widget only.

  • is_currency / prefix / suffix (DecimalField) — render a currency/number input. is_currency is auto-set when both prefix and suffix are empty.

  • views (DateField) — the MUI date-picker views (e.g. ['year', 'month', 'day']), emitted in the schema as date_views.

  • allowed_mime_types and max_file_size (FileField / ImageField) — filter the file picker and enforce size/type on the server.

  • description — documentation only, never shown in the UI; it is read into the schema bundle’s per-field description. Distinct from help_text (which the React UI renders).

Relational editability uses two independent flags:

  • editable — Django’s own attribute. editable=False means the relation is never written through the API: it is still listed and returned nested as a read-only {id, label} value, but the server ignores any submitted value. Use it for server-managed relations such as created_by. The schema reports related_editable: false for these fields.

  • related_editable — package-specific, default True. With False the relation can be set to an existing record (id-based update) but no on-the-fly creation is offered.

They compose: an editable=False field is read-only regardless of related_editable; a writable field with related_editable=False can be changed to an existing record but cannot create a new one.

The package also ships an abstract GenericSimpleModel (a nome plus an auto-slugged unique slug) for label-style models.

PostGIS / geometry fields

The custom fields include the GeoDjango geometry types (PointField, PolygonField, MultiPointField, MultiPolygonField, GeometryCollectionField). A model geometry field is serialized as a GeoJSON GISGeometryField (the React point picker / multipolygon draw widget) instead of DRF’s generic fallback.

GeoDjango is imported only when a model field actually is a GeoDjango field (detected from the field’s class), never for plain fields. The geometry fields are also exported lazily, so importing drf_react_by_schema / drf_react_by_schema.fields never touches GeoDjango unless one is used. This matters because importing GeoDjango triggers GDAL discovery, which on a host without GDAL invokes the C compiler (ctypes.util.find_library runs gcc); doing that for plain fields makes every list serialization extremely slow.

If a project does use geometry fields but GEOS/GDAL are unavailable, the failure is loud rather than silent:

  • a Django system check (drf_react_by_schema.E001) fails manage.py check / runserver / migrate with an actionable message;

  • accessing drf_react_by_schema.fields.PointField (or any geometry field) raises ImproperlyConfigured explaining that GeoDjango failed to import.

To use geometry fields: install GeoDjango’s native libraries (GDAL/GEOS, e.g. libgdal-dev and libgeos-dev on Debian/Ubuntu), add django.contrib.gis to INSTALLED_APPS, and use the django.contrib.gis.db.backends.postgis database engine. You may also set GDAL_LIBRARY_PATH / GEOS_LIBRARY_PATH to skip library discovery.

Global permission-aware queryset filtering

Every generated viewset supports a filter_by_callback (and a list-only filter_list_by_callback) that runs in get_queryset/list/excel and returns filter(**kwargs) arguments (optionally with or_kwargs).

Instead of subclassing each viewset to set them, provide a global fallback callback for all generated viewsets at once:

DRF_REACT_BY_SCHEMA = {
    ...
    # Dotted path to ``def callback(viewset, queryset) -> dict|None``
    # (a 3-arg ``(viewset, queryset, kwargs)`` form is also supported).
    'FILTER_BY_CALLBACK': 'myapp.filters.filter_access_by_permissions',
    'FILTER_LIST_BY_CALLBACK': 'myapp.filters.filter_list_access_by_permissions',
}

The viewset-level attribute always wins; when it is unset, the callback falls back to the global setting (gone too, no filtering happens). A viewset can opt out of the global fallback by setting use_global_filter_by_callback = False.

Because nested and autocomplete viewsets inherit from the same base, the global callback also applies to them, which is useful for scoping pickers and sub-resources to the rows the current user may access.

Grid filters

The grid filter (field / operator / value, each a list, combined with AND) accepts the operators listed in system.conventions.grid_filter of the schema bundle: equals, not, contains, startsWith, endsWith, isEmpty, isNotEmpty, isAnyOf, is, after, before, onOrAfter, onOrBefore, entre, =, !=, >, >=, <, <=. A note on relations: equality on a foreign key compares the related object’s label (its __str__/nome/slug), not its primary key — to filter by pk use field='<fk>__id' (or field='<fk>_id' on a raw model field) with operator is or =.

Date operators (is, not, after, before, onOrAfter, onOrBefore, entre) are day-based when the target is a DateTimeField and the value is a bare date (YYYY-MM-DD): the package applies the __date transform itself, so entrada_prevista with onOrBefore=2026-10-14 includes rows later that day. after/before are strict (they exclude the named day); onOrAfter/onOrBefore are inclusive. Send a time-bearing value to keep time-of-day semantics; an explicit __date path is left untouched and remains day-based.

With USE_TZ=True the day is evaluated in the current timezone (normally settings.TIME_ZONE); with USE_TZ=False the stored naive datetime is used. __date casts the column, so it cannot use a plain btree index: endpoints over large tables should use a filter_by_callback with half-open ranges or an expression index. equals and isAnyOf are text/choice operators, not date operators; use is for date/time columns.

With DRF_REACT_BY_SCHEMA['GRID_FILTER_STRICT'] = True a filter the endpoint cannot resolve raises a 400 naming the field instead of being silently ignored (which would return the unfiltered list). It is off by default so a stale React grid layout does not break the list.

Pagination

Page size is a single source of truth in DRF_REACT_BY_SCHEMA:

  • PAGINATION_PAGE_SIZE — default page size, 50 if unset. The deprecated PAGINATION_PER_PAGE is still read as a fallback (with a DeprecationWarning).

  • PAGINATION_PAGE_SIZE_OPTIONS — the choices offered by the generated React client, [5, 10, 25, 50, 100] if unset. sync_react writes the resolved values to src/drfReactBySchema/pagination.ts (export const paginationConfig), which the app passes to DRFReactBySchemaProvider (pagination prop). The maxPageSize baked into that file is capped at 500 (the MIT DataGrid rejects a larger pageSize); the server accepts more.

  • PAGINATION_MAX_PAGE_SIZE — hard server-side cap, 10000 if unset.

Clients may override the size per request with the page_size query parameter (e.g. ?page=N&page_size=200); the value is capped at PAGINATION_MAX_PAGE_SIZE. The resolved configuration is reported to MCP clients under system.conventions.pagination.

Server-side totals & facets (sum_rows)

List endpoints accept a sum_rows query parameter that aggregates over the whole filtered set (not just the current page), regardless of which page is returned. It is available when server pagination is on (DRF_REACT_BY_SCHEMA['PAGINATION_MODE'] == 'server'); the totals ride in the pagination envelope as sum_rows (and facet breakdowns as facets).

The parameter is a comma-separated list of items. Each item is a measure (optionally faceted):

item := measure [ '|' dim [ '|' dim ... ] [ '|' limit ] ]
  • measure is a numeric field name, count (COUNT(*), i.e. rows), or count_distinct:<field> (COUNT(DISTINCT <field>), counting distinct non-null values).

  • dim is a facet: a ForeignKey (grouped by fk_id), a field with choices, or a BooleanField. Forward relations may be nested with __ (e.g. categoria__regiao groups by the category’s region).

  • a trailing all-digits part is the limit (top-N buckets, ordered by the measure descending). Django field names can never be all-digits, so it is unambiguous.

  • | separates the parts of an item; , separates items.

Flat totals keep their legacy shape {"<measure>_total": value} and every measure — faceted or not — also reports its grand total there. The : of a count_distinct:<field> measure becomes _ in the flat key (e.g. count_distinct_categoria_total).

GET /api/widget/?sum_rows=preco                        # {preco_total: ...}
GET /api/widget/?sum_rows=preco,count                  # two global totals
GET /api/widget/?sum_rows=count|categoria              # rows per category
GET /api/widget/?sum_rows=count|categoria__regiao      # ... by the category's region
GET /api/widget/?sum_rows=count_distinct:categoria     # distinct categories
GET /api/widget/?sum_rows=count_distinct:categoria|tipo  # distinct categories per type
GET /api/widget/?sum_rows=preco|categoria              # price per category (+ preco_total)
GET /api/widget/?sum_rows=preco|categoria|tipo         # cross-tab
GET /api/widget/?sum_rows=preco|tipo|4                 # top-4 types by price

Response (the facets key is null when no facet was requested):

{
  "count": 120,
  "sum_rows": {"preco_total": 4500.0, "count_total": 120},
  "facets": [
    {
      "field": "preco",
      "by": ["categoria"],
      "limit": null,
      "buckets": [
        {"values": {"categoria": {"value": 12, "display_name": "Exemplo"}}, "total": 1200.0},
        {"values": {"categoria": {"value": null, "display_name": "—"}}, "total": 0.0}
      ]
    }
  ],
  "results": ["..."]
}

Each bucket carries values (one {value, display_name} per facet dimension, using the same convention as choice fields elsewhere) and total (the measure). Invalid measures/facets (non-numeric, unknown, non-concrete) are rejected with 400, and the error enumerates the valid options for that model (its numeric fields, its facetable foreign keys / choice / boolean fields, or its distinct-count fields). The valid options are also published per model in the schema bundle as models[<Model>].sum_rows = {"medidas": [...], "dimensoes": [...], "medidas_distintas": [...]}. The same grammar powers the drf-react-by-schema-mcp resumo tool and is understood by the React package.

Machine-readable schema (GET /drbs/schema/)

Besides the per-model OPTIONS schema and the sync_react TS/JSON snapshots written for the React app, the package exposes a schema-only JSON document, meant for programmatic clients such as an MCP server:

GET /drbs/schema/

It is registered at the project root (a sibling of /autocomplete/), so a model named Schema cannot collide with /api/schema/. It returns one object with:

  • stamp — a deterministic SHA-256 (16 hex chars) of the structural bundle, stable across processes and independent of row data; it changes iff the generated picture (models, fields, endpoints, system config) or the package version changes;

  • drbs_version — the installed drf-react-by-schema version;

  • system — apps, AUTH_USER_MODEL, the DRF_REACT_BY_SCHEMA summary, static permission hints (DRF defaults, global, per-model), request conventions (API base, pagination, ordering/search/sum_rows params, grid-filter operators and the relation-filter note) and about (the project’s ABOUT.md, trimmed — see below);

  • endpoints — every top-level route with its path/kind, verbose_name/verbose_name_plural, the underlying Django model and app_label (which can differ from the endpoint key when a viewset aliases another model’s) and the endpoint’s query capabilities: search_fields / ordering_fields (the model paths ?search=/?ordering= accept), filter_fields (the names the grid filter accepts as field) and label_field (the field an equality filter compares when it targets a relation by label);

  • models — each model’s field schema (the OPTIONS POST payload the React app consumes, minus the React-only keys — see below), the Django model/app_label, verbose names, the model class docstring (trimmed), the valid sum_rows measures and dimensions (medidas/dimensoes) and the model’s reverse relations (related): for each child relation, its name, the child model, the linking field, the child endpoint and — when the relation has a generated nested route — the nested_path template (e.g. /api/categoria/<pk>/widget/).

The bundle deliberately omits the React-only TypeScript artifacts (interfaces/enums — still written to types.ts by sync_react), the React-only field keys (schema_bundle.UI_ONLY_FIELD_KEYS: ui_required, model_required, style, model_multiline, model_html, is_currency, prefix/suffix, date_views… — they carry no API semantics) and the auto-generated nested *Related* viewsets. Instead, each relation field in schema carries a related_model naming the model it points at, the endpoint carries its query capabilities and the model carries its reverse relations. The React artifacts are untouched: the slimming and the extra keys live only in the MCP bundle, so sync_react’s output stays byte-identical.

Documentation in the bundle

Free-form, non-UI documentation is folded into the bundle so an assistant can understand the system. It is kept out of the React artifacts on purpose: sync_react’s schemas/*.json stay byte-identical (the bundle is a separate, structural copy — see build_schema_bundle).

  • Model docstrings — a model class’s own docstring (inherited docs are ignored) is reported as models[<Model>].docstring, trimmed at a word boundary to schema_bundle.MODEL_DOCSTRING_MAX characters.

  • Field descriptions — pass description="..." to any package field (CharField, ForeignKey, DecimalField, …); it is reported as models[<Model>].schema[<field>].description (trimmed to FIELD_DESCRIPTION_MAX). This is not help_text: help_text is rendered to end users by the React package, while description documents the field for the API client / assistant. Only values passed explicitly are reported — the field type’s own description is never.

  • Project ``ABOUT.md`` — a project-root ABOUT.md (settings.BASE_DIR) is reported as system.about (trimmed to ABOUT_MAX). Override the path with DRF_REACT_BY_SCHEMA['ABOUT_PATH']; a missing file yields null. Use it for what the project is about — not deploy/contribute instructions.

All three feed the stamp, so changing them invalidates an MCP client’s cached copy; because the bundle is cached per process, a change also needs a process restart to be picked up.

The endpoint sits behind the project’s existing auth (the same permission source used by the generated viewsets — DRF_REACT_BY_SCHEMA['PERMISSIONS'], defaulting to IsAuthenticated; authentication comes from your DRF defaults, so an OAuth read token works). It sets ETag: "<stamp>", honors If-None-Match (returning 304 Not Modified) and caches the built bundle per process. The payload contains structure and static configuration only — no row data and no secrets.

It is gated by DRF_REACT_BY_SCHEMA['SCHEMA_ENDPOINT'] (default True). Set it to False to not register the route at all:

DRF_REACT_BY_SCHEMA = {
    ...
    'SCHEMA_ENDPOINT': False,
}

Upload-first file staging

A file inside an inline formset row is uploaded before the parent form is saved, so the row can carry a plain reference and the request stays JSON (a top-level file still forces multipart; mixing is fine, because the row reference is plain JSON inside key[]).

  • POST /drbs/upload/ — multipart file in, 201 {id, url, name, size} out. The permissions are the same as the generated viewsets (DRF_REACT_BY_SCHEMA['PERMISSIONS']). It is registered at the project root, so a model named Upload (route /api/upload/) cannot collide with it. The endpoint requires the package migrations (see step 6 of the quick start).

  • Model FileField / ImageField serializer fields accept either an UploadedFile (multipart, unchanged) or a reference — the whole {id, ...} object or a bare id. The model write copies the staged file into the target field (so that field’s upload_to/storage apply) and enforces its max_file_size / allowed_mime_types (a violation surfaces as a row-indexed 400).

  • Once the write succeeds, the staging records and files are deleted. A failed request keeps the staged file so the client can retry with the same id.

Account, password change and reset

The package ships re-usable endpoints for the JWT + simplejwt stack. They reuse django.contrib.auth (token generator + password hashing), so no extra package (such as allauth / dj-rest-auth) is required.

Endpoints (auto-registered; password_reset needs no authentication):

  • GET /api/my_account/ — authenticated. Returns MyAccountSerializer for the current user (id, username, first_name, last_name, email, is_active). Override it by defining your own MyAccountSerializer / MyAccountViewSet in the app.

  • POST /api/my_account/change_password/ — authenticated. Body: old_password, new_password.

  • POST /api/password_reset/request/ — {email}. Mints a reset uid``+``token for the user (if the e-mail is registered; replies generically either way to avoid leaking accounts) and forwards them to the delivery callback.

  • POST /api/password_reset/validate/ — {uid, token}. Resolves the account behind a link (name + whether a password is already set) without changing anything, so the frontend can greet the user and tell a first-time invite from a reset.

  • POST /api/password_reset/confirm/ — {uid, token, new_password}. Validates the token and sets the new password.

Sending the reset e-mail is pluggable. The simplest path needs no code: configure the standard Django e-mail settings and the reset-link path, and a default callback sends a plain-text message via Django’s mail backend:

# EMAIL_* (e.g. EMAIL_BACKEND, EMAIL_HOST, ...) must also be configured.
PASSWORD_RESET_CONFIRM_PATH = '/password-reset/confirm/'  # your frontend page
# Optional: override the link's domain/scheme instead of using the request's.
# PASSWORD_RESET_DOMAIN = 'example.com'
# PASSWORD_RESET_SCHEME = 'https'

request_password_reset looks the user up by e-mail directly (not through PasswordResetForm) so accounts with an unusable password — e.g. freshly invited users that still must set their own password — also receive a reset link.

For a customized delivery (branding, HTML, another channel), point DRF_REACT_BY_SCHEMA['PASSWORD_RESET_CALLBACK'] at a callable or a dotted path. The library already mints the uid/token before invoking your callback, so the effective change is a single small function like the one below:

# myapp/auth.py
from django.conf import settings
from django.core.mail import send_mail

def send_reset_email(request, user, uid, token):
    domain = request.get_host()
    scheme = 'https' if request.is_secure() else 'http'
    path = getattr(settings, 'PASSWORD_RESET_CONFIRM_PATH', '/password-reset/confirm/')
    send_mail(
        'Password reset',
        f'Follow this link to set your password: {scheme}://{domain}{path}?uid={uid}&token={token}',
        None,
        [user.email],
    )
DRF_REACT_BY_SCHEMA = {
    ...
    'PASSWORD_RESET_CALLBACK': 'myapp.auth.send_reset_email',
}

Excel export

Every generated viewset exposes GET /api/<model>/excel/, which returns an XLSX file via drf-excel applying the same list filters. The download filename is derived from the model’s verbose_name_plural.

Caching and ETags

list and retrieve responses are cached for 900 seconds and guarded by a single global ETag, so a client can revalidate with If-None-Match. The cache key is scoped per authenticated user (and Vary: Authorization is set), so a row-scoped response is never served to a different user. A successful mutation clears the cache and increments the global ETag version. Redis is recommended for production (see Optional dependencies); Django’s other cache backends work out of the box.

Optional dependencies

For enhanced caching in the automatically generated viewsets:

  1. Redis (recommended for production caching):

    pip install drf-react-by-schema[redis]

    Then configure it in your Django settings:

    CACHES = {
        'default': {
            'BACKEND': 'django_redis.cache.RedisCache',
            'LOCATION': 'redis://127.0.0.1:6379/1',
            'OPTIONS': {
                'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            }
        }
    }
  2. Django’s native cache backends (in-memory, file-based, database, …) are supported out of the box. For example:

    CACHES = {
        'default': {
            'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        }
    }

Running the tests

The suite is self-contained: tests/settings.py configures a minimal Django project and tests/testapp provides the models the tests exercise, so nothing outside this repository is needed. Install the package with its test extra and run pytest:

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

install_requires pins Django>=3.2,<6.0, so this resolves to the newest supported series (5.2). Pin an explicit series if you need a different one, e.g. pip install -e ".[test]" "Django>=4.2,<4.3".

To reproduce the full supported matrix locally, use tox (it creates and manages its own per-interpreter environments and pins each Django series, so no venv is required):

tox -e py312-django52

tox -l lists every combination; CI runs the same matrix on every push.

Issues and source

Report bugs and request features at https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django/-/issues. The source is at https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django.

Metadata

Release files for drf-react-by-schema 0.29.9

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

Source distribution (sdist)

Source distribution for drf-react-by-schema 0.29.9
File Size Uploaded Provenance Actions
drf_react_by_schema-0.29.9.tar.gz 151.3 kB With provenance Details for drf_react_by_schema-0.29.9.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for drf-react-by-schema 0.29.9
File Interpreter ABI Platform Actions
drf_react_by_schema-0.29.9-py3-none-any.whl Python 3 none any Details for drf_react_by_schema-0.29.9-py3-none-any.whl

Total release size: 276.0 kB

Release files / drf_react_by_schema-0.29.9.tar.gz

Download URL drf_react_by_schema-0.29.9.tar.gz
Size 151.3 kB
Tags Source
SHA-256 checksum
How to use checksums
bbc8988155176ab3fe39bdf1a38e52c8983f40f8d885f74db4b90c88b43b135d
BLAKE2b-256 checksum
How to use checksums
83ef272b97746e3865ecd62f35ece11759cf0c2dc28e2c23012cc0910c73b54f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.16

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitLab CI/CD, verified by PyPI on Nov 9, 2026.

Transparency log

Release files / drf_react_by_schema-0.29.9-py3-none-any.whl

Download URL drf_react_by_schema-0.29.9-py3-none-any.whl
Size 124.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8b657c3fc4dc75d759d4610f4e1152884850b8db5c332767e4e1e0a6a1eec389
BLAKE2b-256 checksum
How to use checksums
ac00dbabb45d07cd7bea09f2583122df198dbd47d552deceea4a9c77904e81dc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.16

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitLab CI/CD, verified by PyPI on Nov 9, 2026.

Transparency log

Security for drf-react-by-schema 0.29.9

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.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for drf_react_by_schema-0.29.9.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitLab CI/CD, verified by PyPI on Nov 9, 2026.

Transparency log

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.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.29.9 This release

2 release files Full provenance

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