Skip to content

Repository files navigation

Welcome to the FastAPI Dynamic Form Documentation!

License PyPI Release Supported Python Versions Pre-commit Coverage Open Issues Last Commit Languages

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.

Project Detail

  • Language: Python >= 3.9
  • Framework: FastAPI >= 0.110
  • SQLAlchemy: >= 2.0 (async)
  • Pydantic: >= 2.7

Documentation Overview

The documentation is organized into the following sections:


Quick Start

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.

1. Install the Package

Option 1: Using pip (Recommended)

Install the package via pip:

$ pip install fastapi-dynamic-form

Option 2: Using Poetry

If you're using Poetry, add the package with:

$ poetry add fastapi-dynamic-form

Option 3: Using pipenv

If you're using pipenv, install the package with:

$ pipenv install fastapi-dynamic-form

2. Install a Database Driver

The 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    # SQLite

3. Include the Router

Mount 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")

4. Connect a Database

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_session

5. Create the Schema

For 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.metadata

The package creates four tables: dynamic_forms, dynamic_form_fields, field_types and form_submissions.

6. Seed the Field Types

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.

7. Wire Up Authentication

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_dependency

Because 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.

8. (Optional) Validate Your Configuration

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
    yield

API Guide

This 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 API Management

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.

Dynamic Forms (/admin/forms/)

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."}}.

Dynamic Fields (/admin/forms/{form_id}/fields/)

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."}

Field Types (/admin/field-types/)

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.

Form Submissions (/admin/submissions/)

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.


User API Management

These endpoints are open to everyone by default. They only ever expose active forms, active field types, and the requesting user's own submissions.

Forms (/forms/)

  • 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.

Fields (/fields/)

  • List (GET /fields/): Retrieve the fields belonging to active forms.
  • Retrieve (GET /fields/{field_id}/): Retrieve a single field.

Field Types (/field-types/)

  • List (GET /field-types/): Retrieve the active field types.
  • Retrieve (GET /field-types/{field_type_id}/): Retrieve a single active field type.

Form Submissions (/submissions/)

  • 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."}

Response Fields

Forms

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.

Fields

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 Types

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.

Form Submissions

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.

Example Responses

Retrieve a form with its fieldsGET /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 listGET /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 formPOST /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": []}
}

Throttling

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_CLASSES settings 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=[])

Ordering, Filtering and Search

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's ORDERING_FIELDS setting are permitted; anything else returns 400.
  • search: a free-text term matched case-insensitively against that resource's SEARCH_FIELDS.
  • filters: each name in that resource's FILTER_FIELDS becomes an exact-match query parameter, e.g. ?is_active=false or ?form_id=3. Values are coerced to the column type, and a value that does not fit returns 422.
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.


Pagination

List endpoints use limit/offset pagination and return the DRF-style envelope: count, next, previous, results.

  • limit defaults to 10, with a minimum of 1 and a maximum of 100.
  • A limit below the minimum or one that is not a number falls back to the default; a limit above the maximum is capped.
  • offset defaults to 0; negative and non-numeric offsets are treated as 0.

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 = 500

Permissions

Permissions are ordinary FastAPI dependencies rather than DRF permission classes.

  • Admin endpoints run the callable named by DYNAMIC_FORM_API_ADMIN_PERMISSION_DEPENDENCY, which defaults to is_admin_user and raises 403 for anyone who is not staff.
  • Any endpoint can take an extra dependency through its EXTRA_PERMISSION_DEPENDENCY setting. The package provides is_authenticated_user, which raises 401 for 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 user
settings.configure(
    DYNAMIC_FORM_API_ADMIN_PERMISSION_DEPENDENCY="myapp.permissions.is_form_editor"
)

Disabling Endpoints

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

Settings can be supplied in two ways, checked in this order:

  1. 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,
    )
  2. Through environment variables of the same name. Values are parsed as JSON where possible, so DYNAMIC_FORM_API_DYNAMIC_FORM_ALLOW_LIST=false yields a real boolean and DYNAMIC_FORM_API_DYNAMIC_FORM_SEARCH_FIELDS='["name"]' yields a real list. List settings also accept a plain comma-separated string, such as name,description.

Important: schema classes are resolved when the routers are imported, exactly as the Django viewsets resolve serializer_class at class-definition time. Call settings.configure(...) before importing dynamic_form.api.routers if you override a *_SCHEMA_CLASS setting. Every other setting is read per request and can be changed at any time.

Example Settings

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,
)

Settings Overview

Below is a detailed description of each setting, so you can better understand and tweak them to fit your project's needs.


DYNAMIC_FORM_BASE_USER_THROTTLE_RATE

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.


DYNAMIC_FORM_STAFF_USER_THROTTLE_RATE

Type: str

Default: "100/minute"

Description: The request rate applied to staff users, using the same format as above.


DYNAMIC_FORM_API_ADMIN_PERMISSION_DEPENDENCY

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.


DYNAMIC_FORM_API_CURRENT_USER_DEPENDENCY

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.


DYNAMIC_FORM_API_USER_SCHEMA_CLASS

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.


DYNAMIC_FORM_API_USER_SCHEMA_FIELDS

Type: List[str]

Default: ["id", "username"]

Description: The attributes that make up a user representation.


DYNAMIC_FORM_API_<RESOURCE>_SCHEMA_CLASS

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.


DYNAMIC_FORM_API_<RESOURCE>_ORDERING_FIELDS

Type: List[str]

Description: The fields the ordering query parameter is allowed to name. Requesting any other field returns 400.


DYNAMIC_FORM_API_<RESOURCE>_SEARCH_FIELDS

Type: List[str]

Description: The fields the search query parameter is matched against, case-insensitively. May be empty, which disables search for that resource.


DYNAMIC_FORM_API_<RESOURCE>_FILTER_FIELDS

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.


DYNAMIC_FORM_API_<RESOURCE>_THROTTLE_CLASSES

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.


DYNAMIC_FORM_API_<RESOURCE>_PAGINATION_CLASS

Type: Optional[str]

Default: "dynamic_form.api.paginations.DefaultLimitOffSetPagination"

Description: The import path of the paginator used for the resource's list endpoint.


DYNAMIC_FORM_API_<RESOURCE>_EXTRA_PERMISSION_DEPENDENCY

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.


DYNAMIC_FORM_API_<RESOURCE>_ALLOW_LIST

Type: bool

Default: True

Description: Whether the resource's list endpoint is enabled. A disabled action returns 405.


DYNAMIC_FORM_API_<RESOURCE>_ALLOW_RETRIEVE

Type: bool

Default: True

Description: Whether the resource's retrieve endpoint is enabled.


DYNAMIC_FORM_API_<RESOURCE>_ALLOW_CREATE

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.


DYNAMIC_FORM_API_<RESOURCE>_ALLOW_UPDATE

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.


DYNAMIC_FORM_API_<RESOURCE>_ALLOW_DELETE

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.


Differences from dj-dynamic-form

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_*_PERMISSION and DYNAMIC_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 by FILTER_FIELDS.

Two behavioural differences are worth calling out:

  • FormSubmission.user_id is 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 carry user_id rather than a nested user object.
  • PUT and PATCH are separate routes. The DRF serializers supported partial updates through one endpoint; here PUT takes the full create schema and PATCH takes an all-optional update schema.

Contributing

Contributions are welcome. Please see CONTRIBUTING.md for the development setup, and run the test suite before opening a pull request:

$ pytest

The suite enforces 100% statement coverage.

License

This project is licensed under the MIT License — see the LICENSE file for details.

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages