Server-rendered form components and layout helpers for Pydantic models
Project description
Pydantic SchemaForms
Support Python Versions
SonarCloud:
Note: This project should be considered in beta as it is actively under development and may have breaking changes.
Overview
pydantic-schemaforms is a modern Python library that generates dynamic HTML forms from Pydantic 2.x+ models.
It is designed for server-rendered apps: you define a model (and optional UI hints) and get back ready-to-embed HTML with validation and framework styling.
Key Features:
- 🚀 Zero-Configuration Forms: Generate complete HTML forms directly from Pydantic models
- 🎨 Multi-Framework Support: Bootstrap, Material Design, Tailwind CSS, and custom frameworks
- ✅ Built-in Validation: Client-side HTML5 + server-side Pydantic validation
- 🔧 JSON-Schema-form style UI hints: Uses a familiar
ui_element,ui_autofocus,ui_optionsvocabulary - 📱 Responsive & Accessible: Mobile-first design with full ARIA support
- 🌐 Framework Ready: First-class Flask and FastAPI helpers, plus plain HTML for other stacks
Important:
submit_urlis required when rendering forms. The library does not choose a default submit target.
Documentation
- Docs site: https://devsetgo.github.io/pydantic-schemaforms/
- Live Demo: https://pydantic-schemaforms.devsetgo.com
- Source: https://github.com/devsetgo/pydantic-schemaforms
Requirements
- Python 3.14+
- Pydantic 2.7+ (included in library)
Quick Start
Install
pip install pydantic-schemaforms
FastAPI (async / ASGI)
This is the recommended “drop-in HTML” pattern for FastAPI: define a FormModel and call render_form_html().
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from pydantic import ValidationError
from pydantic_schemaforms.enhanced_renderer import render_form_html
from pydantic_schemaforms.schema_form import Field, FormModel
class MinimalLoginForm(FormModel):
username: str = Field(
title="Username",
ui_autofocus=True,
ui_placeholder="demo_user",
)
password: str = Field(
title="Password",
ui_element="password",
)
remember_me: bool = Field(
default=False,
title="Remember me",
ui_element="checkbox",
)
app = FastAPI()
@app.api_route("/login", methods=["GET", "POST"], response_class=HTMLResponse)
async def login(request: Request, style: str = "bootstrap"):
form_data = {}
errors = {}
if request.method == "POST":
submitted = dict(await request.form())
form_data = submitted
try:
MinimalLoginForm(**submitted)
except ValidationError as e:
errors = {err["loc"][0]: err["msg"] for err in e.errors() if err.get("loc")}
else:
# optional demo data
form_data = {"username": "demo_user", "remember_me": True}
form_html = render_form_html(
MinimalLoginForm,
framework=style,
form_data=form_data,
errors=errors,
submit_url="/login",
)
return f"""<!doctype html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<title>Login</title>
<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">
</head>
<body class=\"container my-5\">
<h1 class=\"mb-4\">Login</h1>
{form_html}
</body>
</html>"""
Run it:
pip install "pydantic-schemaforms[fastapi]" uvicorn
uvicorn main:app --reload
FastAPI: simple registration page
This mirrors the in-repo example apps: your host page loads Bootstrap, and render_form_html() returns form markup (plus any inline helper scripts), ready to embed.
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from pydantic import ValidationError
from pydantic_schemaforms.enhanced_renderer import render_form_html
from pydantic_schemaforms.schema_form import FormModel, Field
class UserRegistrationForm(FormModel):
username: str = Field(title="Username", min_length=3)
email: str = Field(title="Email", ui_element="email")
password: str = Field(title="Password", ui_element="password", min_length=8)
app = FastAPI()
@app.api_route("/register", methods=["GET", "POST"], response_class=HTMLResponse)
async def register(request: Request):
form_data = {}
errors = {}
if request.method == "POST":
submitted = dict(await request.form())
form_data = submitted
try:
UserRegistrationForm(**submitted)
except ValidationError as e:
errors = {err["loc"][0]: err["msg"] for err in e.errors() if err.get("loc")}
form_html = render_form_html(
UserRegistrationForm,
framework="bootstrap",
form_data=form_data,
errors=errors,
submit_url="/register",
)
return f"""<!doctype html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<title>Register</title>
<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">
</head>
<body class=\"container my-5\">
<h1 class=\"mb-4\">Register</h1>
{form_html |safe}
</body>
</html>"""
Flask (sync / WSGI)
In synchronous apps (Flask), the simplest pattern is the same: define a FormModel and call render_form_html().
from flask import Flask, request
from pydantic import ValidationError
from pydantic_schemaforms.enhanced_renderer import render_form_html
from pydantic_schemaforms.schema_form import Field, FormModel
class MinimalLoginForm(FormModel):
username: str = Field(title="Username", ui_autofocus=True)
password: str = Field(title="Password", ui_element="password")
remember_me: bool = Field(default=False, title="Remember me", ui_element="checkbox")
app = Flask(__name__)
@app.route("/login", methods=["GET", "POST"])
def login():
form_data = {}
errors = {}
if request.method == "POST":
submitted = request.form.to_dict()
form_data = submitted
try:
MinimalLoginForm(**submitted)
except ValidationError as e:
errors = {err["loc"][0]: err["msg"] for err in e.errors() if err.get("loc")}
form_html = render_form_html(
MinimalLoginForm,
framework="bootstrap",
form_data=form_data,
errors=errors,
submit_url="/login",
)
return f"""<!doctype html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<title>Login</title>
<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">
</head>
<body class=\"container my-5\">
<h1 class=\"mb-4\">Login</h1>
{form_html|safe}
</body>
</html>"""
Flask: simple registration page
from flask import Flask, request
from pydantic import ValidationError
from pydantic_schemaforms.enhanced_renderer import render_form_html
from pydantic_schemaforms.schema_form import FormModel, Field
class UserRegistrationForm(FormModel):
username: str = Field(title="Username", min_length=3)
email: str = Field(title="Email", ui_element="email")
password: str = Field(title="Password", ui_element="password", min_length=8)
app = Flask(__name__)
@app.route("/register", methods=["GET", "POST"])
def register():
form_data = {}
errors = {}
if request.method == "POST":
submitted = request.form.to_dict()
form_data = submitted
try:
UserRegistrationForm(**submitted)
except ValidationError as e:
errors = {err["loc"][0]: err["msg"] for err in e.errors() if err.get("loc")}
form_html = render_form_html(
UserRegistrationForm,
framework="bootstrap",
form_data=form_data,
errors=errors,
submit_url="/register",
)
return f"""<!doctype html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<title>Register</title>
<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">
</head>
<body class=\"container my-5\">
<h1 class=\"mb-4\">Register</h1>
{form_html|safe}
</body>
</html>"""
UI vocabulary compatibility
The library supports a JSON-Schema-form style vocabulary (UI hints like input types and options), but you can also stay “pure Pydantic” and let the defaults drive everything.
See the docs site for the current, supported UI hint patterns.
Framework Support
Bootstrap 5 (Recommended)
UserForm.render_form(framework="bootstrap", submit_url="/submit")
- Complete Bootstrap integration
- Form validation states and styling
- Responsive grid system
- Custom form controls
Note: Bootstrap markup/classes are always generated, but Bootstrap CSS/JS are only included if your host template provides them or you opt into self_contained=True / include_framework_assets=True.
Self-contained Bootstrap (no host template assets)
If you want a single HTML string that includes Bootstrap CSS/JS inline (no CDN, no global layout requirements), use the self_contained=True convenience flag:
from pydantic_schemaforms.enhanced_renderer import render_form_html
form_html = render_form_html(
UserRegistrationForm,
framework=style,
form_data=form_data,
debug=debug,
self_contained=True,
submit_url="/register",
)
You can also call the FormModel convenience if you prefer:
form_html = UserRegistrationForm.render_form(
data=form_data,
framework=style,
debug=debug,
self_contained=True,
submit_url="/register",
)
Material Design
UserForm.render_form(framework="material", submit_url="/submit")
- Materialize CSS framework
- Floating labels and animations
- Material icons integration
Plain HTML
UserForm.render_form(framework="none", submit_url="/submit")
- Clean HTML5 forms
- No framework dependencies
- Easy to style with custom CSS
- FastAPI example support: append
?style=none(for example:/login?style=none&demo=true)
Renderer Architecture
- EnhancedFormRenderer is the canonical renderer. It walks the Pydantic
FormModel, feeds the sharedLayoutEngine, and delegates chrome/assets to aRendererTheme. - ModernFormRenderer now piggybacks on Enhanced by generating a throwaway
FormModelfrom legacyFormDefinition/FormFieldhelpers. It exists so existing builder/integration code keeps working while still benefiting from the shared pipeline. (The oldPy314Rendereralias has been removed; importModernFormRendererdirectly when you need the builder DSL.)
Because everything flows through Enhanced, fixes to layout, validation, or framework themes immediately apply to every renderer (Bootstrap, Material, embedded/self-contained, etc.). Choose the renderer based on the API surface you prefer (Pydantic models for FormModel or the builder DSL for ModernFormRenderer); the generated HTML is orchestrated by the same core engine either way.
Advanced Examples
File Upload Form
class FileUploadForm(FormModel):
title: str = Field(..., description="Upload title")
files: str = Field(
...,
description="Select files",
ui_element="file",
ui_options={"accept": ".pdf,.docx", "multiple": True}
)
description: str = Field(
...,
description="File description",
ui_element="textarea",
ui_options={"rows": 3}
)
Event Creation Form
class EventForm(FormModel):
event_name: str = Field(..., description="Event name", ui_autofocus=True)
event_datetime: str = Field(
...,
description="Event date and time",
ui_element="datetime-local"
)
max_attendees: int = Field(
...,
ge=1,
le=1000,
description="Maximum attendees",
ui_element="number"
)
is_public: bool = Field(
True,
description="Make event public",
ui_element="checkbox"
)
theme_color: str = Field(
"#3498db",
description="Event color",
ui_element="color"
)
Form Validation
from pydantic import ValidationError
@app.route("/submit", methods=["POST"])
def handle_submit():
try:
# Validate form data using your Pydantic model
user_data = UserForm(**request.form)
# Process valid data
return f"Welcome {user_data.username}!"
except ValidationError as e:
# Handle validation errors
errors = e.errors()
return f"Validation failed: {errors}", 400
Flask Integration
Complete Flask application example:
from flask import Flask, request, render_template_string
from pydantic import ValidationError
from pydantic_schemaforms.schema_form import FormModel, Field
app = Flask(__name__)
class UserRegistrationForm(FormModel):
username: str = Field(
...,
min_length=3,
max_length=20,
description="Choose a unique username",
ui_autofocus=True
)
email: str = Field(
...,
description="Your email address",
ui_element="email"
)
password: str = Field(
...,
min_length=8,
description="Choose a secure password",
ui_element="password"
)
age: int = Field(
...,
ge=13,
le=120,
description="Your age",
ui_element="number"
)
newsletter: bool = Field(
False,
description="Subscribe to our newsletter",
ui_element="checkbox"
)
@app.route("/", methods=["GET", "POST"])
def registration():
if request.method == "POST":
try:
# Validate form data
user = UserRegistrationForm(**request.form)
return f"Registration successful for {user.username}!"
except ValidationError as e:
errors = e.errors()
# Re-render form with errors
form_html = UserRegistrationForm.render_form(
framework="bootstrap",
submit_url="/",
errors=errors
)
return render_template_string(BASE_TEMPLATE, form_html=form_html)
# Render empty form
form_html = UserRegistrationForm.render_form(framework="bootstrap", submit_url="/")
return render_template_string(BASE_TEMPLATE, form_html=form_html)
BASE_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>User Registration</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-5">
<h1>User Registration</h1>
{{ form_html | safe }}
</div>
</body>
</html>
"""
if __name__ == "__main__":
app.run(debug=True)
Render Timing
The library automatically measures form rendering time with multiple display options:
Display Timing Below Submit Button
Add a small timing display to your form:
html = render_form_html(MyForm, show_timing=True, submit_url="/submit")
This shows: Rendered in 0.0045s
Display in Debug Panel
Include comprehensive debugging information:
html = render_form_html(MyForm, debug=True, submit_url="/submit")
Shows timing in the debug panel header: Debug panel (development only) — 0.0045s render
Automatic INFO-Level Logging
Timing is always logged at INFO level:
import logging
logging.basicConfig(level=logging.INFO)
html = render_form_html(MyForm, submit_url="/submit")
# Logs: INFO pydantic_schemaforms.enhanced_renderer: Form rendered in 0.0045s
Use Cases:
- Development: Use
show_timing=Trueto see performance quickly - Debugging: Use
debug=Trueto see form structure and timing - Production: Timing is logged automatically at INFO level for monitoring
See Render Timing Docs for complete details.
Application Logging
The library provides optional DEBUG-level logging that respects your application's logging configuration:
Automatic Timing Logs
Timing is always logged at INFO level (for production monitoring):
import logging
from pydantic_schemaforms import render_form_html
logging.basicConfig(level=logging.INFO)
html = render_form_html(MyForm, submit_url="/submit")
# Timing is logged automatically
Optional Debug Logs
Enable DEBUG logging to see detailed rendering steps:
import logging
from pydantic_schemaforms import render_form_html
# Option 1: Application-level DEBUG
logging.basicConfig(level=logging.DEBUG)
html = render_form_html(MyForm, submit_url="/submit")
# ✅ Timing + debug logs appear
# Option 2: Per-render control
html = render_form_html(MyForm, enable_logging=True, submit_url="/submit")
# ✅ Debug logs appear for this render only
Selective Logger Configuration
Enable library debugging without affecting your app's logging:
import logging
# Application at INFO level
logging.basicConfig(level=logging.INFO)
# Library DEBUG logs
library_logger = logging.getLogger('pydantic_schemaforms')
library_logger.setLevel(logging.DEBUG)
html = render_form_html(MyForm, submit_url="/submit")
# ✅ Library debug logs visible
# ✅ App remains at INFO level
Best Practice: Use Approach 1 (application-level configuration) in most cases. The library respects your app's logging setup.
See Application Logging Docs for complete details and integration examples.
Examples in This Repository
The main runnable demo in this repo is the FastAPI example:
- Run:
make ex-run - Visit: http://localhost:8000
- Self-contained demo: http://localhost:8000/self-contained
See examples/fastapi_example.py and examples/shared_models.py for the complete implementation.
Logging and timing examples:
- Timing Options Example - Display options for render timing
- Timing Demo - Complete timing feature demonstration
- Logging Example - Logging configuration patterns
- Logging Control Example - Fine-grained logging control
Supported Input Types
Text Inputs:
text(default),email,password,searchtel,urltextarea
Numeric Inputs:
number,range
Date/Time Inputs:
date,time,datetime-localweek,month
Selection Inputs:
checkbox,radio,select
Specialized Inputs:
file,color,hidden
Input Options:
All HTML5 input attributes are supported through ui_options or Field parameters.
API Reference
FormModel
Extend your Pydantic models with FormModel to add form rendering capabilities:
from pydantic_schemaforms.schema_form import FormModel, Field
class MyForm(FormModel):
field_name: str = Field(..., ui_element="email")
# Render Bootstrap markup (expects host page to load Bootstrap)
html = MyForm.render_form(framework="bootstrap", submit_url="/submit")
# Render fully self-contained Bootstrap HTML (inlines vendored Bootstrap CSS/JS)
html = MyForm.render_form(framework="bootstrap", submit_url="/submit", self_contained=True)
Field Function
Enhanced Field function with UI element support:
Field(
default=..., # Pydantic default value
description="Label", # Field label
ui_element="email", # Input type
ui_autofocus=True, # Auto-focus field
ui_options={...}, # Additional options
# All standard Pydantic Field options...
)
Framework Options
"bootstrap"- Bootstrap 5 styling (recommended)"material"- Material Design (Materialize CSS)"none"- Plain HTML5 forms
Contributing
Contributions are welcome! Please check out the Contributing Guide for details.
Development Setup:
git clone https://github.com/devsetgo/pydantic-schemaforms.git
cd pydantic-schemaforms
pip install -e .
Run Tests:
python -m pytest tests/
Links
- Documentation: pydantic-schemaforms Docs
- Repository: GitHub
- PyPI: pydantic-schemaforms
- Issues: Bug Reports & Feature Requests
License
This project is licensed under the MIT License - see the LICENSE file for details: https://github.com/devsetgo/pydantic-schemaforms/blob/main/LICENSE
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydantic_schemaforms-26.1.8b0.tar.gz.
File metadata
- Download URL: pydantic_schemaforms-26.1.8b0.tar.gz
- Upload date:
- Size: 282.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b83cb434904789a223ffc3a9fa8a944a8fcbff3b6805790d27ff0e30c02e9c20
|
|
| MD5 |
e164794bdc30fffcaecd7ec4e24a1caf
|
|
| BLAKE2b-256 |
ff57cbff9af05417de157b765864d3095be7e6ad33ac99a588f74b7452775bb3
|
Provenance
The following attestation bundles were made for pydantic_schemaforms-26.1.8b0.tar.gz:
Publisher:
pythonpublish.yml on devsetgo/pydantic-schemaforms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_schemaforms-26.1.8b0.tar.gz -
Subject digest:
b83cb434904789a223ffc3a9fa8a944a8fcbff3b6805790d27ff0e30c02e9c20 - Sigstore transparency entry: 1004625525
- Sigstore integration time:
-
Permalink:
devsetgo/pydantic-schemaforms@f7269adc4811322b617c1a728f063bb1b6050cd2 -
Branch / Tag:
refs/tags/26.1.8.beta - Owner: https://github.com/devsetgo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pythonpublish.yml@f7269adc4811322b617c1a728f063bb1b6050cd2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pydantic_schemaforms-26.1.8b0-py3-none-any.whl.
File metadata
- Download URL: pydantic_schemaforms-26.1.8b0-py3-none-any.whl
- Upload date:
- Size: 302.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1aeb0a0e02fbbb5f99e852b49f2bf1a595296c61a704f38a9145ecce3462f6f9
|
|
| MD5 |
ea99c695114fd130b8621a33093dc244
|
|
| BLAKE2b-256 |
b531b1f7195bed8865972b31faba4ba32e75cfe646338e88df347d7d19375a5c
|
Provenance
The following attestation bundles were made for pydantic_schemaforms-26.1.8b0-py3-none-any.whl:
Publisher:
pythonpublish.yml on devsetgo/pydantic-schemaforms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_schemaforms-26.1.8b0-py3-none-any.whl -
Subject digest:
1aeb0a0e02fbbb5f99e852b49f2bf1a595296c61a704f38a9145ecce3462f6f9 - Sigstore transparency entry: 1004625529
- Sigstore integration time:
-
Permalink:
devsetgo/pydantic-schemaforms@f7269adc4811322b617c1a728f063bb1b6050cd2 -
Branch / Tag:
refs/tags/26.1.8.beta - Owner: https://github.com/devsetgo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pythonpublish.yml@f7269adc4811322b617c1a728f063bb1b6050cd2 -
Trigger Event:
release
-
Statement type: