fastapi-dynamic-form is a FastAPI package developed by Lazarus that empowers developers to create, manage, and process dynamic forms within FastAPI applications.
It provides a robust framework for defining flexible form structures, fields, and submissions, with built-in support for RESTful API integration, async SQLAlchemy persistence, and Pydantic validation.
It is a port of dj-dynamic-form: the data model, the URL layout, the settings names, and the error messages are all preserved, so a client written against the Django package works unchanged.
- Language: Python >= 3.9
- Framework: FastAPI >= 0.110
- SQLAlchemy: >= 2.0 (async)
- Pydantic: >= 2.7
The documentation is organized into the following sections:
- Quick Start: Get up and running quickly with basic setup instructions.
- API Guide: Detailed information on available APIs and endpoints.
- Settings: Configuration options and settings you can customize.
- Differences from dj-dynamic-form: What changed in the port, and why.
This section provides a fast and easy guide to getting the fastapi-dynamic-form package up and running in your FastAPI
project. Follow the steps below to quickly set up the package and start using it.
Option 1: Using pip (Recommended)
Install the package via pip:
$ pip install fastapi-dynamic-formOption 2: Using Poetry
If you're using Poetry, add the package with:
$ poetry add fastapi-dynamic-formOption 3: Using pipenv
If you're using pipenv, install the package with:
$ pipenv install fastapi-dynamic-formThe package speaks async SQLAlchemy, so you need an async driver for your database:
$ pip install asyncpg # PostgreSQL
$ pip install aiomysql # MySQL / MariaDB
$ pip install aiosqlite # SQLiteMount the package router on your application. Everything the package exposes lives on a single APIRouter:
from fastapi import FastAPI
from dynamic_form.api.routers import router
app = FastAPI()
app.include_router(router, prefix="/api/v1")You have two options, depending on whether your project already manages its own SQLAlchemy engine.
Option 1: Let the package own the engine
from contextlib import asynccontextmanager
from fastapi import FastAPI
from dynamic_form.api.routers import router
from dynamic_form.db import configure_database, seed_field_types
@asynccontextmanager
async def lifespan(app: FastAPI):
database = configure_database("postgresql+asyncpg://user:pass@localhost/app")
await database.create_all()
async with database.session_factory() as session:
await seed_field_types(session)
yield
await database.dispose()
app = FastAPI(lifespan=lifespan)
app.include_router(router, prefix="/api/v1")Option 2: Reuse your own session dependency (recommended)
If your project already has a session dependency, override the package one and it will be used everywhere:
from dynamic_form.db import get_session
app.dependency_overrides[get_session] = my_get_sessionFor small projects and tests, create_all() is enough. For anything real, point Alembic at the package metadata so
the tables are managed alongside your own:
# alembic/env.py
from dynamic_form.models import Base
target_metadata = Base.metadataThe package creates four tables: dynamic_forms, dynamic_form_fields, field_types and form_submissions.
seed_field_types() replaces the data migration shipped with the Django package. It is idempotent, so calling it on
every startup is safe:
from dynamic_form.db import seed_field_types
async with database.session_factory() as session:
await seed_field_types(session)It creates ten field types: text, number, email, boolean, date, dropdown, textarea, checkbox, radio
and file.
The package has no opinion about who your users are. By default every request is anonymous, which means the admin endpoints are closed and submissions are stored without an owner.
To connect your own authentication, override the get_current_user dependency. Anything you return is used as the
user, and the package reads three attributes from it: id, is_staff and is_authenticated.
from dynamic_form.api.dependencies import get_current_user
app.dependency_overrides[get_current_user] = my_current_user_dependencyBecause this is a normal FastAPI dependency override, your dependency can declare sub-dependencies of its own (OAuth2 schemes, database lookups, and so on).
Alternatively, point DYNAMIC_FORM_API_CURRENT_USER_DEPENDENCY at a
callable that takes the request and returns a user.
The package can check its settings on startup, in the same spirit as Django's system check framework:
from dynamic_form.settings.checks import run_checks
@asynccontextmanager
async def lifespan(app: FastAPI):
run_checks() # raises ImproperlyConfigured on a bad configuration
yieldThis section provides a detailed overview of the API endpoints, including the available actions on each and how the responses are shaped.
All paths below are relative to the prefix you mounted the router under. Every path ends in a trailing slash, matching the Django package.
Admin endpoints require a staff user. By default that means the current user must have is_staff = True; see
DYNAMIC_FORM_API_ADMIN_PERMISSION_DEPENDENCY to change the rule.
Full management of form definitions, including the inactive ones.
- List (
GET /admin/forms/): Retrieve a paginated list of every form. - Retrieve (
GET /admin/forms/{form_id}/): Retrieve a single form with its fields. - Create (
POST /admin/forms/): Define a new form. - Replace (
PUT /admin/forms/{form_id}/): Replace a form definition. - Update (
PATCH /admin/forms/{form_id}/): Partially update a form definition. - Delete (
DELETE /admin/forms/{form_id}/): Delete a form and its fields.
Form names are unique. Creating or renaming onto a taken name returns 400 with
{"detail": {"name": "A form with this name already exists."}}.
Fields are managed underneath their parent form, so a field can never be reached through the wrong form's URL.
- List (
GET /admin/forms/{form_id}/fields/): Retrieve the fields of one form. - Retrieve (
GET /admin/forms/{form_id}/fields/{field_id}/): Retrieve a single field. - Create (
POST /admin/forms/{form_id}/fields/): Add a field to the form. - Replace (
PUT /admin/forms/{form_id}/fields/{field_id}/): Replace a field definition. - Update (
PATCH /admin/forms/{form_id}/fields/{field_id}/): Partially update a field. - Delete (
DELETE /admin/forms/{form_id}/fields/{field_id}/): Remove a field.
Creating a field validates three things, each returning 400:
| Condition | Response |
|---|---|
| The parent form is missing or inactive | {"form": "Specified form not found or inactive."} |
The field_type_id does not exist |
{"field_type_id": "Field Type with the given ID was not found."} |
| The name is taken inside this form | {"name": "A field with this name already exists in the specified form."} |
Manage the field types available to form builders, including deactivated ones.
- List (
GET /admin/field-types/): Retrieve every field type. - Retrieve (
GET /admin/field-types/{field_type_id}/): Retrieve a single field type. - Create (
POST /admin/field-types/): Define a custom field type. - Replace (
PUT /admin/field-types/{field_type_id}/): Replace a field type. - Update (
PATCH /admin/field-types/{field_type_id}/): Partially update a field type. - Delete (
DELETE /admin/field-types/{field_type_id}/): Delete a field type.
Field type names are unique and limited to 20 characters. Deleting a type still referenced by a field is rejected by
the database, mirroring the PROTECT behaviour of the Django model.
Read access to every submission, from every user.
- List (
GET /admin/submissions/): Retrieve every submission. - Retrieve (
GET /admin/submissions/{submission_id}/): Retrieve any submission. - Create (
POST /admin/submissions/): Disabled by default. - Update (
PATCH /admin/submissions/{submission_id}/): Disabled by default. - Delete (
DELETE /admin/submissions/{submission_id}/): Disabled by default.
Submissions are treated as an audit record, so the admin write endpoints are off unless you turn them on. See Disabling Endpoints.
These endpoints are open to everyone by default. They only ever expose active forms, active field types, and the requesting user's own submissions.
- List (
GET /forms/): Retrieve a paginated list of active forms. - Retrieve (
GET /forms/{form_id}/): Retrieve a single active form with its fields.
Requesting an inactive form returns 404.
- List (
GET /fields/): Retrieve the fields belonging to active forms. - Retrieve (
GET /fields/{field_id}/): Retrieve a single field.
- List (
GET /field-types/): Retrieve the active field types. - Retrieve (
GET /field-types/{field_type_id}/): Retrieve a single active field type.
- List (
GET /submissions/): Retrieve the current user's submissions. - Retrieve (
GET /submissions/{submission_id}/): Retrieve one of them. - Create (
POST /submissions/): Submit a form. - Update (
PUT/PATCH /submissions/{submission_id}/): Disabled by default. - Delete (
DELETE /submissions/{submission_id}/): Disabled by default.
A submission is attached to the current user when the request is authenticated, and stored anonymously otherwise. Anonymous callers cannot list or retrieve submissions, since there is no owner to match on.
Submitting validates the payload against the form definition, each failure returning 400:
| Condition | Response |
|---|---|
submitted_data is empty |
{"submitted_data": "This field may not be null."} |
| The form is missing or inactive | {"form_id": "Form with the given ID was not found or is inactive."} |
| A required field is absent | {"<field name>": "This field is required."} |
| Field | Type | Description |
|---|---|---|
id |
integer | The unique identifier of the form. |
name |
string | The unique name of the form. |
description |
string / null | Optional description of the form. |
is_active |
boolean | Whether the form accepts submissions. |
created_at |
datetime | When the form was created. |
updated_at |
datetime | When the form was last modified. |
fields |
array | The fields that make up this form, in display order. |
| Field | Type | Description |
|---|---|---|
id |
integer | The unique identifier of the field. |
form_id |
integer | The form this field belongs to. |
name |
string | The field name, unique within its form. |
field_type |
object | The nested field type definition. |
label |
string / null | Display label, falling back to the name. |
is_required |
boolean | Whether the field must be filled. |
choices |
JSON / null | Options for dropdown, radio and checkbox fields. |
default_value |
JSON / null | Value used to prefill the field. |
validation_rules |
JSON / null | Custom validation constraints. |
order |
integer | Position of the field in the form layout. |
| Field | Type | Description |
|---|---|---|
id |
integer | The unique identifier of the field type. |
name |
string | Short unique identifier, at most 20 characters. |
label |
string | Human-readable name shown to users. |
description |
string / null | Optional explanation of the type. |
created_at |
datetime | When the type was added. |
is_active |
boolean | Whether the type can be selected for new fields. |
| Field | Type | Description |
|---|---|---|
id |
integer | The unique identifier of the submission. |
form_id |
integer | The form this submission belongs to. |
form |
object | The nested form definition. |
user_id |
string / null | The submitting user, when authenticated. |
submitted_data |
object | The data submitted by the user. |
submitted_at |
datetime | When the submission was made. |
Retrieve a form with its fields — GET /api/v1/forms/1/
{
"id": 1,
"name": "Contact Us",
"description": "Get in touch.",
"is_active": true,
"created_at": "2026-07-20T09:15:00Z",
"updated_at": "2026-07-20T09:15:00Z",
"fields": [
{
"id": 1,
"form_id": 1,
"name": "email",
"label": "Your Email",
"is_required": true,
"choices": null,
"default_value": null,
"validation_rules": {"max_length": 255},
"order": 1,
"field_type": {
"id": 3,
"name": "email",
"label": "Email Field",
"description": "An email address input.",
"created_at": "2026-07-20T09:00:00Z",
"is_active": true
}
}
]
}A paginated list — GET /api/v1/forms/?limit=2
{
"count": 5,
"next": "http://localhost:8000/api/v1/forms/?limit=2&offset=2",
"previous": null,
"results": [
{"id": 1, "name": "Contact Us", "is_active": true, "fields": []},
{"id": 2, "name": "Feedback", "is_active": true, "fields": []}
]
}Submit a form — POST /api/v1/submissions/
{
"form_id": 1,
"submitted_data": {"email": "someone@example.com"}
}{
"id": 42,
"form_id": 1,
"user_id": "7",
"submitted_data": {"email": "someone@example.com"},
"submitted_at": "2026-07-20T10:30:00Z",
"form": {"id": 1, "name": "Contact Us", "is_active": true, "fields": []}
}The package ships a role-based rate limiter that applies a different rate to staff and non-staff users, configured
through DYNAMIC_FORM_BASE_USER_THROTTLE_RATE and
DYNAMIC_FORM_STAFF_USER_THROTTLE_RATE.
Rates use the format {number}/{time_unit}, where the unit is one of second, minute, hour or day. Exceeding
the rate returns 429 with a Retry-After header.
Authenticated requests are bucketed by user id; anonymous requests fall back to the client IP address, preferring
X-Forwarded-For when present.
Note: request history is kept in process memory. That is correct for a single worker, but each worker in a multi-process deployment tracks its own counts. For shared limits across workers, subclass
BaseRateThrottle, back it with Redis, and point the*_THROTTLE_CLASSESsettings at your class.
To disable throttling for a resource, set its throttle classes to an empty list:
settings.configure(DYNAMIC_FORM_API_DYNAMIC_FORM_THROTTLE_CLASSES=[])Every list endpoint accepts three kinds of query parameter:
ordering: a comma-separated list of fields. Prefix a field with-for descending order, e.g.?ordering=-created_at,name. Only the fields in that resource'sORDERING_FIELDSsetting are permitted; anything else returns400.search: a free-text term matched case-insensitively against that resource'sSEARCH_FIELDS.- filters: each name in that resource's
FILTER_FIELDSbecomes an exact-match query parameter, e.g.?is_active=falseor?form_id=3. Values are coerced to the column type, and a value that does not fit returns422.
GET /api/v1/admin/forms/?search=contact&ordering=-created_at&is_active=true
This replaces the django-filter integration of the Django package; there is no separate filterset class to install.
List endpoints use limit/offset pagination and return the DRF-style envelope: count, next, previous, results.
limitdefaults to10, with a minimum of1and a maximum of100.- A limit below the minimum or one that is not a number falls back to the default; a limit above the maximum is capped.
offsetdefaults to0; negative and non-numeric offsets are treated as0.
To change the bounds, subclass the paginator and point the relevant PAGINATION_CLASS setting at it:
from dynamic_form.api.paginations import DefaultLimitOffSetPagination
class LargePagination(DefaultLimitOffSetPagination):
default_limit = 50
max_limit = 500Permissions are ordinary FastAPI dependencies rather than DRF permission classes.
- Admin endpoints run the callable named by
DYNAMIC_FORM_API_ADMIN_PERMISSION_DEPENDENCY, which defaults tois_admin_userand raises403for anyone who is not staff. - Any endpoint can take an extra dependency through its
EXTRA_PERMISSION_DEPENDENCYsetting. The package providesis_authenticated_user, which raises401for anonymous callers.
A permission dependency is a plain callable that may be sync or async, and may declare a request parameter, a
user parameter, both, or neither:
# myapp/permissions.py
from fastapi import HTTPException
def is_form_editor(user):
if "forms:write" not in getattr(user, "scopes", []):
raise HTTPException(status_code=403, detail="Not allowed.")
return usersettings.configure(
DYNAMIC_FORM_API_ADMIN_PERMISSION_DEPENDENCY="myapp.permissions.is_form_editor"
)Each action on each resource can be turned off. A disabled action stays visible in the OpenAPI schema but returns
405:
{
"detail": "The method \"DELETE\" is currently disabled for this endpoint. It can be changed in the settings."
}settings.configure(
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_ALLOW_DELETE=False,
DYNAMIC_FORM_API_FORM_SUBMISSION_ALLOW_CREATE=False,
)Settings can be supplied in two ways, checked in this order:
-
In code, through the settings registry:
from dynamic_form.settings import settings settings.configure( DYNAMIC_FORM_BASE_USER_THROTTLE_RATE="60/minute", DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_ALLOW_DELETE=False, )
-
Through environment variables of the same name. Values are parsed as JSON where possible, so
DYNAMIC_FORM_API_DYNAMIC_FORM_ALLOW_LIST=falseyields a real boolean andDYNAMIC_FORM_API_DYNAMIC_FORM_SEARCH_FIELDS='["name"]'yields a real list. List settings also accept a plain comma-separated string, such asname,description.
Important: schema classes are resolved when the routers are imported, exactly as the Django viewsets resolve
serializer_classat class-definition time. Callsettings.configure(...)before importingdynamic_form.api.routersif you override a*_SCHEMA_CLASSsetting. Every other setting is read per request and can be changed at any time.
Below is an example configuration with all available settings and their defaults:
from dynamic_form.settings import settings
settings.configure(
# Global throttle settings
DYNAMIC_FORM_BASE_USER_THROTTLE_RATE="30/minute",
DYNAMIC_FORM_STAFF_USER_THROTTLE_RATE="100/minute",
# Global API settings
DYNAMIC_FORM_API_ADMIN_PERMISSION_DEPENDENCY="dynamic_form.api.dependencies.permissions.is_admin_user",
DYNAMIC_FORM_API_CURRENT_USER_DEPENDENCY=None,
DYNAMIC_FORM_API_USER_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_USER_SCHEMA_FIELDS=["id", "username"],
# DynamicForm API settings
DYNAMIC_FORM_API_DYNAMIC_FORM_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_DYNAMIC_FORM_ORDERING_FIELDS=["created_at", "updated_at"],
DYNAMIC_FORM_API_DYNAMIC_FORM_SEARCH_FIELDS=["name", "description"],
DYNAMIC_FORM_API_DYNAMIC_FORM_FILTER_FIELDS=["is_active"],
DYNAMIC_FORM_API_DYNAMIC_FORM_THROTTLE_CLASSES="dynamic_form.api.throttlings.RoleBasedUserRateThrottle",
DYNAMIC_FORM_API_DYNAMIC_FORM_PAGINATION_CLASS="dynamic_form.api.paginations.DefaultLimitOffSetPagination",
DYNAMIC_FORM_API_DYNAMIC_FORM_EXTRA_PERMISSION_DEPENDENCY=None,
DYNAMIC_FORM_API_DYNAMIC_FORM_ALLOW_LIST=True,
DYNAMIC_FORM_API_DYNAMIC_FORM_ALLOW_RETRIEVE=True,
# Admin DynamicForm API settings
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_ORDERING_FIELDS=["created_at", "updated_at"],
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_SEARCH_FIELDS=["name", "description"],
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_FILTER_FIELDS=["is_active"],
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_THROTTLE_CLASSES="dynamic_form.api.throttlings.RoleBasedUserRateThrottle",
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_PAGINATION_CLASS="dynamic_form.api.paginations.DefaultLimitOffSetPagination",
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_EXTRA_PERMISSION_DEPENDENCY=None,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_ALLOW_LIST=True,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_ALLOW_RETRIEVE=True,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_ALLOW_CREATE=True,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_ALLOW_UPDATE=True,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FORM_ALLOW_DELETE=True,
# DynamicField API settings
DYNAMIC_FORM_API_DYNAMIC_FIELD_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_DYNAMIC_FIELD_ORDERING_FIELDS=["order"],
DYNAMIC_FORM_API_DYNAMIC_FIELD_SEARCH_FIELDS=["name", "label"],
DYNAMIC_FORM_API_DYNAMIC_FIELD_FILTER_FIELDS=["form_id", "is_required"],
DYNAMIC_FORM_API_DYNAMIC_FIELD_THROTTLE_CLASSES="dynamic_form.api.throttlings.RoleBasedUserRateThrottle",
DYNAMIC_FORM_API_DYNAMIC_FIELD_PAGINATION_CLASS="dynamic_form.api.paginations.DefaultLimitOffSetPagination",
DYNAMIC_FORM_API_DYNAMIC_FIELD_EXTRA_PERMISSION_DEPENDENCY=None,
DYNAMIC_FORM_API_DYNAMIC_FIELD_ALLOW_LIST=True,
DYNAMIC_FORM_API_DYNAMIC_FIELD_ALLOW_RETRIEVE=True,
# Admin DynamicField API settings
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_ORDERING_FIELDS=["order"],
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_SEARCH_FIELDS=["name", "label"],
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_FILTER_FIELDS=["form_id", "is_required"],
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_THROTTLE_CLASSES="dynamic_form.api.throttlings.RoleBasedUserRateThrottle",
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_PAGINATION_CLASS="dynamic_form.api.paginations.DefaultLimitOffSetPagination",
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_EXTRA_PERMISSION_DEPENDENCY=None,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_ALLOW_LIST=True,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_ALLOW_RETRIEVE=True,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_ALLOW_CREATE=True,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_ALLOW_UPDATE=True,
DYNAMIC_FORM_API_ADMIN_DYNAMIC_FIELD_ALLOW_DELETE=True,
# FieldType API settings
DYNAMIC_FORM_API_FIELD_TYPE_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_FIELD_TYPE_ORDERING_FIELDS=["name", "created_at"],
DYNAMIC_FORM_API_FIELD_TYPE_SEARCH_FIELDS=["name", "label", "description"],
DYNAMIC_FORM_API_FIELD_TYPE_FILTER_FIELDS=["is_active"],
DYNAMIC_FORM_API_FIELD_TYPE_THROTTLE_CLASSES="dynamic_form.api.throttlings.RoleBasedUserRateThrottle",
DYNAMIC_FORM_API_FIELD_TYPE_PAGINATION_CLASS="dynamic_form.api.paginations.DefaultLimitOffSetPagination",
DYNAMIC_FORM_API_FIELD_TYPE_EXTRA_PERMISSION_DEPENDENCY=None,
DYNAMIC_FORM_API_FIELD_TYPE_ALLOW_LIST=True,
DYNAMIC_FORM_API_FIELD_TYPE_ALLOW_RETRIEVE=True,
# Admin FieldType API settings
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_ORDERING_FIELDS=["name", "created_at"],
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_SEARCH_FIELDS=["name", "label", "description"],
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_FILTER_FIELDS=["is_active"],
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_THROTTLE_CLASSES="dynamic_form.api.throttlings.RoleBasedUserRateThrottle",
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_PAGINATION_CLASS="dynamic_form.api.paginations.DefaultLimitOffSetPagination",
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_EXTRA_PERMISSION_DEPENDENCY=None,
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_ALLOW_LIST=True,
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_ALLOW_RETRIEVE=True,
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_ALLOW_CREATE=True,
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_ALLOW_UPDATE=True,
DYNAMIC_FORM_API_ADMIN_FIELD_TYPE_ALLOW_DELETE=True,
# FormSubmission API settings
DYNAMIC_FORM_API_FORM_SUBMISSION_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_FORM_SUBMISSION_ORDERING_FIELDS=["submitted_at"],
DYNAMIC_FORM_API_FORM_SUBMISSION_SEARCH_FIELDS=[],
DYNAMIC_FORM_API_FORM_SUBMISSION_FILTER_FIELDS=["form_id"],
DYNAMIC_FORM_API_FORM_SUBMISSION_THROTTLE_CLASSES="dynamic_form.api.throttlings.RoleBasedUserRateThrottle",
DYNAMIC_FORM_API_FORM_SUBMISSION_PAGINATION_CLASS="dynamic_form.api.paginations.DefaultLimitOffSetPagination",
DYNAMIC_FORM_API_FORM_SUBMISSION_EXTRA_PERMISSION_DEPENDENCY=None,
DYNAMIC_FORM_API_FORM_SUBMISSION_ALLOW_LIST=True,
DYNAMIC_FORM_API_FORM_SUBMISSION_ALLOW_RETRIEVE=True,
DYNAMIC_FORM_API_FORM_SUBMISSION_ALLOW_CREATE=True,
DYNAMIC_FORM_API_FORM_SUBMISSION_ALLOW_UPDATE=False,
DYNAMIC_FORM_API_FORM_SUBMISSION_ALLOW_DELETE=False,
# Admin FormSubmission API settings
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_SCHEMA_CLASS=None,
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_ORDERING_FIELDS=["submitted_at"],
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_SEARCH_FIELDS=[],
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_FILTER_FIELDS=["form_id"],
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_THROTTLE_CLASSES="dynamic_form.api.throttlings.RoleBasedUserRateThrottle",
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_PAGINATION_CLASS="dynamic_form.api.paginations.DefaultLimitOffSetPagination",
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_EXTRA_PERMISSION_DEPENDENCY=None,
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_ALLOW_LIST=True,
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_ALLOW_RETRIEVE=True,
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_ALLOW_CREATE=False,
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_ALLOW_UPDATE=False,
DYNAMIC_FORM_API_ADMIN_FORM_SUBMISSION_ALLOW_DELETE=False,
)Below is a detailed description of each setting, so you can better understand and tweak them to fit your project's needs.
Type: str
Default: "30/minute"
Description: The request rate applied to non-staff and anonymous users. Uses the format
{number}/{time_unit}, where the unit is one of second, minute, hour or day.
Type: str
Default: "100/minute"
Description: The request rate applied to staff users, using the same format as above.
Type: Optional[str]
Default: "dynamic_form.api.dependencies.permissions.is_admin_user"
Description: The import path of the callable that guards every admin endpoint. It receives the current user and
should raise an HTTPException to deny access. Set to None to leave the admin endpoints unguarded, which is only
appropriate when something in front of the application already restricts them.
Type: Optional[str]
Default: None
Description: The import path of a callable that resolves the current user from the request. When unset, every
request is anonymous unless you override the get_current_user dependency, which is the preferred approach because it
supports sub-dependencies.
Type: Optional[str]
Default: None
Description: The import path of the schema used to represent a user. Falls back to the built-in UserSchema.
This is an extension point for applications that override a submission schema to embed a full user object.
Type: List[str]
Default: ["id", "username"]
Description: The attributes that make up a user representation.
Type: Optional[str]
Default: None
Description: The import path of the schema used for a resource's responses. Falls back to the built-in schema.
Because schemas are resolved when the routers are imported, this must be configured before importing
dynamic_form.api.routers.
Type: List[str]
Description: The fields the ordering query parameter is allowed to name. Requesting any other field returns
400.
Type: List[str]
Description: The fields the search query parameter is matched against, case-insensitively. May be empty, which
disables search for that resource.
Type: List[str]
Description: The columns exposed as exact-match query parameters. Values are coerced to the column type, and a
value that does not fit returns 422. May be empty.
Type: Optional[Union[str, List[str]]]
Default: "dynamic_form.api.throttlings.RoleBasedUserRateThrottle"
Description: One import path or a list of them, naming the throttle classes applied to the resource. Set to an empty list to disable throttling.
Type: Optional[str]
Default: "dynamic_form.api.paginations.DefaultLimitOffSetPagination"
Description: The import path of the paginator used for the resource's list endpoint.
Type: Optional[str]
Default: None
Description: The import path of an additional permission callable run on every request to the resource, on top of
the admin check. dynamic_form.api.dependencies.permissions.is_authenticated_user is provided for the common case of
requiring authentication.
Type: bool
Default: True
Description: Whether the resource's list endpoint is enabled. A disabled action returns 405.
Type: bool
Default: True
Description: Whether the resource's retrieve endpoint is enabled.
Type: bool
Default: True for admin form, field and field-type resources and for user submissions; False for admin
submissions.
Description: Whether the resource's create endpoint is enabled.
Type: bool
Default: True for admin form, field and field-type resources; False for submissions.
Description: Whether the resource's PUT and PATCH endpoints are enabled.
Type: bool
Default: True for admin form, field and field-type resources; False for submissions.
Description: Whether the resource's delete endpoint is enabled.
<RESOURCE> is one of: DYNAMIC_FORM, ADMIN_DYNAMIC_FORM, DYNAMIC_FIELD, ADMIN_DYNAMIC_FIELD, FIELD_TYPE,
ADMIN_FIELD_TYPE, FORM_SUBMISSION, ADMIN_FORM_SUBMISSION.
The non-admin form, field and field-type resources are read-only, so their ALLOW_CREATE, ALLOW_UPDATE and
ALLOW_DELETE settings are always False and cannot be enabled.
The data model, URL layout, settings names and error messages are preserved. The differences below come from the gap between the two frameworks.
| Area | dj-dynamic-form | fastapi-dynamic-form |
|---|---|---|
| ORM | Django ORM | async SQLAlchemy 2.0 |
| Validation | DRF serializers | Pydantic v2 schemas, with database-dependent checks in the repository layer |
| Schema creation | Django migrations | Base.metadata for Alembic, or create_all() |
| Field type seeding | data migration | idempotent seed_field_types() |
| Views | DRF viewsets and routers | APIRouter modules |
| Permissions | DRF permission classes | FastAPI dependencies |
| Filtering | django-filter filtersets |
FILTER_FIELDS exact-match query parameters |
| Throttle storage | Django cache | in-process memory, subclassable for Redis |
| Settings source | django.conf.settings |
settings.configure() or environment variables |
| Config validation | Django system checks | run_checks() at startup |
| Admin panel | Django admin | none; the /admin/... API endpoints are the equivalent |
Three settings families have no FastAPI analogue and were dropped:
DYNAMIC_FORM_ADMIN_HAS_*_PERMISSIONandDYNAMIC_FORM_ADMIN_SITE_CLASS, which configure the Django admin.DYNAMIC_FORM_API_*_PARSER_CLASSES, since FastAPI performs content negotiation from the endpoint signature.DYNAMIC_FORM_API_*_FILTERSET_CLASS, replaced byFILTER_FIELDS.
Two behavioural differences are worth calling out:
FormSubmission.user_idis an opaque string, not a foreign key. The package cannot know the host application's user table, so it stores the identifier returned by your current-user dependency instead of constraining it. Submission responses therefore carryuser_idrather than a nested user object.PUTandPATCHare separate routes. The DRF serializers supported partial updates through one endpoint; herePUTtakes the full create schema andPATCHtakes an all-optional update schema.
Contributions are welcome. Please see CONTRIBUTING.md for the development setup, and run the test suite before opening a pull request:
$ pytestThe suite enforces 100% statement coverage.
This project is licensed under the MIT License — see the LICENSE file for details.