Pydantic SchemaForms
| Description | Server-rendered form components and layout helpers for Pydantic models |
| Author(s) | Mike Ryan |
| Repository | https://github.com/devsetgo/pydantic-schemaforms |
| Copyright | Copyright © 2025 - 2026 Mike Ryan |
Table of Contents
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_options vocabulary
- π± 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")
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")
Plain HTML¶
UserForm.render_form(framework="none", submit_url="/submit")
?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=True to see performance quickly
- Debugging: Use debug=True to 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, search
- tel, url
- textarea
Numeric Inputs:
- number, range
Date/Time Inputs:
- date, time, datetime-local
- week, 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
Quick Start¶
This page shows two common ways to integrate pydantic-schemaforms into an app:
- Model-first rendering (
FormModel+render_form_html()) - Builder + handlers (legacy):
- Build a
FormBuilder(often viacreate_form_from_model()) - Use exactly one handler per runtime:
- Sync:
handle_form() - Async:
handle_form_async()
- Sync:
- Build a
Option A: Model-first rendering (recommended)¶
from pydantic_schemaforms import Field, FormModel, render_form_html
class User(FormModel):
name: str = Field(...)
email: str = Field(..., ui_element="email")
html = render_form_html(User, submit_url="/user")
Async (FastAPI / ASGI)¶
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from pydantic_schemaforms import Field, FormModel, render_form_html_async
class User(FormModel):
name: str = Field(...)
email: str = Field(..., ui_element="email")
app = FastAPI()
@app.api_route("/user", methods=["GET", "POST"], response_class=HTMLResponse)
async def user_form(request: Request):
form_data = {}
errors = {}
if request.method == "POST":
submitted = dict(await request.form())
form_data = submitted
try:
User(**submitted)
except Exception as exc:
errors = {"form": str(exc)}
form_html = await render_form_html_async(
User,
form_data=form_data,
errors=errors,
submit_url="/user",
)
return f"""
<!doctype html>
<html>
<body>
<h1>User</h1>
{form_html}
</body>
</html>
"""
You can also call await User.render_form_async(...) directly if you prefer a model method.
If your host page already loads Bootstrap/Material, keep defaults. If you want a fully self-contained HTML chunk, pass `self_contained=True`.
See: `docs/configuration.md` and `docs/assets.md`.
## 1) Build a form from a Pydantic model
```python
from pydantic import BaseModel, EmailStr
from pydantic_schemaforms import create_form_from_model
class User(BaseModel):
name: str
email: EmailStr
builder = create_form_from_model(User, framework="bootstrap")
2) Async integration (FastAPI / ASGI)¶
from fastapi import FastAPI, Request
from pydantic_schemaforms import create_form_from_model, handle_form_async
app = FastAPI()
@app.api_route("/user", methods=["GET", "POST"])
async def user_form(request: Request):
builder = create_form_from_model(User, framework="bootstrap")
if request.method == "POST":
form = await request.form()
result = await handle_form_async(builder, submitted_data=dict(form))
if result.get("success"):
return {"ok": True, "data": result["data"]}
return result["form_html"]
result = await handle_form_async(builder)
return result["form_html"]
3) Sync integration (Flask / WSGI)¶
from flask import Flask, request
from pydantic_schemaforms import create_form_from_model, handle_form
app = Flask(__name__)
@app.route("/user", methods=["GET", "POST"])
def user_form():
builder = create_form_from_model(User, framework="bootstrap")
if request.method == "POST":
result = handle_form(builder, submitted_data=request.form.to_dict())
if result.get("success"):
return f"Saved: {result['data']}"
return result["form_html"]
return handle_form(builder)["form_html"]
Notes¶
handle_form*()returns either{form_html}(initial render) or{success: bool, ...}(submission).- Asset delivery (
asset_mode) and full-page wrappers are documented indocs/assets.md.
Tutorials
Tutorial: A Simple FastAPI Project¶
This tutorial walks through creating a small FastAPI app that renders a form from a Pydantic model using pydantic-schemaforms.
It uses the async-first render API so large forms wonβt block the event loop.
Prerequisites¶
- Python 3.14+
Note: model-first rendering¶
This tutorial uses the model-first API (recommended). You only need:
- Define a
FormModel - Render it (async) with
render_form_html_async()orFormModel.render_form_async() - Drop
{{ form_html | safe }}into your template
1) Create a project¶
mkdir schemaforms-fastapi-demo
cd schemaforms-fastapi-demo
python -m venv .venv
source .venv/bin/activate
2) Install dependencies¶
pip install "pydantic-schemaforms[fastapi]" uvicorn
3) Create main.py¶
Create a file named main.py:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from pydantic_schemaforms import Field, FormModel, render_form_html_async
class User(FormModel):
name: str = Field(...)
email: str = Field(..., ui_element="email")
app = FastAPI(title="SchemaForms Demo")
@app.api_route("/user", methods=["GET", "POST"], response_class=HTMLResponse)
async def user_form(request: Request):
form_data = {}
errors = {}
if request.method == "POST":
submitted = dict(await request.form())
form_data = submitted
try:
User(**submitted)
except Exception as exc:
errors = {"form": str(exc)}
form_html = await render_form_html_async(
User,
form_data=form_data,
errors=errors,
submit_url="/user",
)
return f"""
<html>
<body>
<h1>User</h1>
{form_html}
</body>
</html>
"""
You can also use await User.render_form_async(...) if you prefer a model method.
4) Run the server¶
uvicorn main:app --reload
Open http://127.0.0.1:8000/user
Sync vs Async (whatβs the difference?)¶
render_form_html() (sync)¶
Use render_form_html() when your web framework is synchronous (WSGI) and you already have submitted data as a plain dict.
Typical environments:
- Flask / Django (classic request/response)
- CLI apps or scripts that validate a dict
Example (Flask):
from flask import Flask, request
from pydantic_schemaforms import Field, FormModel, render_form_html
class User(FormModel):
name: str = Field(...)
email: str = Field(..., ui_element="email")
app = Flask(__name__)
@app.route("/user", methods=["GET", "POST"])
def user_form():
if request.method == "POST":
form_data = request.form.to_dict()
errors = {}
try:
User(**form_data)
except Exception as exc:
errors = {"form": str(exc)}
return render_form_html(User, form_data=form_data, errors=errors, submit_url="/user")
return render_form_html(User, submit_url="/user")
render_form_html_async() (async)¶
Use render_form_html_async() when you are in an async runtime (ASGI) and you are already await-ing things (like request.form() in FastAPI/Starlette).
Typical environments:
- FastAPI / Starlette
- Any async stack where you want to keep the request handler non-blocking
Important FastAPI note¶
FastAPIβs Request.form() is async, so the most natural implementation is an async def route and render_form_html_async().
If you already have a dict of submitted data (for example from a different parsing path), you can still call the sync renderer inside an async def route β but for large forms, the async renderer avoids blocking the event loop.
Next steps¶
- Learn about asset delivery (
asset_mode) indocs/assets.md - See the broader integration pattern in
docs/quickstart.md
Reference
Configuration¶
This library is driven almost entirely by render-time options (which framework/theme to target, whether to inline assets, what layout to use) plus field-level UI metadata stored in your modelβs JSON Schema.
Rendering entry points¶
submit_url is required for render calls. This library does not default form submit targets.
You can render forms in a few different ways. Pick one that matches your project style:
1) Model-first (recommended)¶
Use FormModel + render_form_html().
from pydantic_schemaforms import Field, FormModel
from pydantic_schemaforms.enhanced_renderer import render_form_html
class RegistrationForm(FormModel):
name: str = Field(..., ui_placeholder="Jane")
email: str = Field(..., ui_element="email")
form_html = render_form_html(
RegistrationForm,
submit_url="/register",
framework="bootstrap",
layout="vertical",
)
If you prefer a method on the model, use RegistrationForm.render_form(...).
2) Builder + handlers (legacy integration)¶
Docs and examples may still reference the builder pattern:
- Build with
create_form_from_model() - Validate + render with
handle_form()/handle_form_async()
This remains supported for backwards compatibility, but the underlying HTML rendering flows through the same enhanced renderer pipeline.
Framework and assets¶
There are two separate but related concepts:
- Framework selection:
framework="bootstrap" | "material" | "none" - Asset delivery: whether the form HTML includes the framework CSS/JS
include_framework_assets¶
False(default): the returned HTML assumes your page already loads Bootstrap/Material.True: the renderer emits framework CSS/JS tags.
asset_mode¶
Controls how the framework assets are provided when include_framework_assets=True:
"vendored": inline the vendored CSS/JS into the output (offline-friendly)."cdn": link to a CDN."none": emit no framework tags.
self_contained¶
For convenience, self_contained=True forces a fully-embedded result:
include_framework_assets=Trueasset_mode="vendored"
html = render_form_html(
RegistrationForm,
submit_url="/register",
self_contained=True,
)
See also: docs/assets.md
Layout selection¶
At the top level, pass layout= to the renderer:
"vertical"(default)"tabbed""side-by-side"
html = render_form_html(RegistrationForm, layout="tabbed", submit_url="/register")
For advanced composition (tabs/accordion/grid wrappers and schema-defined layout fields), see docs/layouts.md.
Field UI metadata¶
UI metadata is stored in json_schema_extra with keys like ui_element, ui_placeholder, etc. The library provides a convenience wrapper pydantic_schemaforms.Field() that populates these keys.
Common UI keys:
ui_element: widget type (see docs/inputs.md)ui_placeholderui_help_textui_options: widget-specific options (e.g. selection choices)ui_class,ui_styleui_disabled,ui_readonly,ui_hidden,ui_autofocusui_order: field ordering
Example:
class ProfileForm(FormModel):
bio: str = Field(
"",
title="Bio",
description="A short bio shown publicly",
ui_element="textarea",
ui_placeholder="Tell us about yourselfβ¦",
ui_options={"rows": 6},
ui_order=10,
)
Escaping and templates (|safe)¶
- If you return the HTML string directly from a framework response (e.g. FastAPI
HTMLResponse), no extra escaping happens. - If you embed the HTML into a Jinja template, you must mark it safe:
{{ form_html | safe }}
Otherwise Jinja will escape the markup and youβll see literal <div> tags in the browser.
Error rendering behavior¶
When you pass errors= to render_form_html() / render_form_html_async(), the renderer now includes a built-in top-level summary block inside form_html.
- Field paths are humanized for users (example:
pets[7].nameβPet #8 β Name). - The same behavior works for Bootstrap and Material output.
- No template-side error loop is required for standard usage.
This means most templates only need:
{{ form_html | safe }}
Layout support behavior¶
The enhanced renderer injects a small internal style block to keep nested/layout-heavy forms (layout, model_list, tabbed/side-by-side structures) width-safe across host templates.
- This reduces the need for route-specific template CSS hacks.
- If your app provides strict custom CSS, you can still override these classes in your host stylesheet.
Inputs (UI Elements)¶
This page documents the supported ui_element values and their expected options.
Where you set these:
- Preferred:
pydantic_schemaforms.Field(..., ui_element="...") - Or directly via
json_schema_extra={"ui_element": "..."}
from pydantic_schemaforms import Field, FormModel
class Example(FormModel):
email: str = Field(..., ui_element="email")
Supported ui_element values¶
These map to concrete input components in pydantic_schemaforms.inputs.*.
Text¶
text(default)passwordemailsearchtextareaurltel
Notes:
- Long string fields may auto-infer to
textarea. passwordpreserves the value if you supply one (use with care).
Numbers¶
numberrange
Selection¶
selectmultiselectcheckboxradiotoggle(aliases:toggle_switch,checkbox_toggle)combobox
Options for selection widgets:
- Provide choices via
ui_options={"options": [...]}orui_options={"choices": [...]}. - Or use JSON Schema enums (e.g.
Literal[...]/Enum) and the renderer will infer options.
Example:
class Preferences(FormModel):
favorite_color: str = Field(
...,
ui_element="select",
ui_options={
"options": [
{"value": "red", "label": "Red"},
{"value": "blue", "label": "Blue"},
]
},
)
Date/time¶
datetimedatetime(alias:datetime-local)monthweek
Specialized¶
filecolorhiddenssn(alias:social_security_number)phone(alias:phone_number)credit_card(aliases:card,cc_number)currency(alias:money)
These specialized elements are opt-in and will not override normal text fields.
Use them explicitly when you want built-in formatting/pattern behavior.
Pseudo elements¶
These are handled specially by the renderer (not standard inputs):
layout: layout-only schema fields (see docs/layouts.md)model_list: repeatable nested model items
Unknown elements¶
If you set ui_element to an unsupported value, the renderer falls back to a basic text input.
If you need a custom widget:
- Implement a
BaseInputsubclass - Register it at runtime via
pydantic_schemaforms.inputs.registry.register_input_class()
(Then you can use your custom ui_element key in schemas.)
Layouts¶
There are two layers of layout support:
1) Top-level layout modes (layout= when you render the form)
2) Composable layout primitives (horizontal/grid/tabs/etc) for advanced composition
1) Top-level layout modes¶
Pass layout= to render_form_html() / FormModel.render_form().
Supported values:
vertical(default)tabbed: groups fields into tabs automaticallyside-by-side: renders fields in two-column rows
from pydantic_schemaforms.enhanced_renderer import render_form_html
html = render_form_html(MyFormModel, layout="side-by-side")
2) Layout primitives (advanced)¶
The module pydantic_schemaforms.rendering.layout_engine contains reusable wrappers:
HorizontalLayoutVerticalLayoutGridLayoutResponsiveGridLayoutTabLayoutAccordionLayoutCardLayoutModalLayout
A convenience factory is provided:
LayoutComposer(aliases:Layout,LayoutFactory)
Example:
from pydantic_schemaforms.rendering.layout_engine import Layout
layout = Layout.grid(
"<div>Left</div>",
"<div>Right</div>",
columns="1fr 2fr",
gap="1rem",
)
html = layout.render(framework="bootstrap", renderer=my_renderer, data={}, errors={})
Schema-defined layout fields¶
A schema field with ui_element="layout" is treated as a layout field.
At render time, the renderer will:
- Call a custom layout renderer if you configured one (
layout_handler/layout_renderer) - Or, if the field value is a
BaseLayoutinstance, call its.render()
This is intentionally an advanced feature (useful for complex nested forms and custom layout engines).
Registering custom layout renderers¶
You can register a named layout renderer:
from pydantic_schemaforms.rendering.layout_engine import LayoutEngine
def my_layout_renderer(field_name, field_schema, value, ui_info, context, engine):
return "<div>Custom layout output</div>"
LayoutEngine.register_layout_renderer("my_layout", my_layout_renderer)
Then set ui_options (or schema ui) to reference it:
layout_handler="my_layout"orlayout_renderer="my_layout"
Notes¶
tabbedgrouping is heuristic-based (field-name keywords). If you need deterministic tabbing, use explicit layout fields or custom renderers.- Layout fields can include nested form markup via
EnhancedFormRenderer.render_form_fields_only().
Assets & asset_mode¶
pydantic-schemaforms is offline-by-default: by default, rendered HTML ships all required JS/CSS from this library (vendored assets are embedded/packaged).
This page documents the standard knobs used across entry points to control asset injection.
Terminology¶
- Vendored assets: Third-party JS/CSS copied into this repo under
pydantic_schemaforms/assets/vendor/**. - Pinned: Versions are recorded in
pydantic_schemaforms/assets/vendor/vendor_manifest.jsonalong withsha256checksums and source URLs. asset_mode: How a renderer should include assets.
asset_mode values¶
Most APIs accept asset_mode with these values:
"vendored"(default)- No external network required.
-
Assets are inlined (e.g.,
<script>β¦</script>/<style>β¦</style>) from the packaged vendor files. -
"cdn"(explicit opt-in) - Emits
<script src="β¦">/<link href="β¦">tags pointing at a CDN. -
URLs are pinned to the versions in the vendored manifest.
-
"none" - Emits no assets.
- Useful when your host app provides its own asset pipeline.
Entry points¶
Legacy wrapper: render_form_html()¶
File: pydantic_schemaforms/render_form.py
asset_mode="vendored" | "cdn" | "none"include_framework_assets: whether to include framework CSS/JS (Bootstrap/Materialize) in the returned HTML.- HTMX is included by default (vendored inline) because this wrapper historically assumed HTMX.
- IMask is available but not injected unless requested.
Example:
from pydantic_schemaforms.render_form import render_form_html
html = render_form_html(
MyForm,
framework="bootstrap",
asset_mode="vendored",
include_framework_assets=True, # inline Bootstrap CSS/JS for self-contained HTML
include_imask=True, # enable when you use masked inputs
)
If you already provide Bootstrap/Materialize in your host app (a global layout, bundler, etc.), keep include_framework_assets=False and use asset_mode="none" or "vendored" depending on whether you still want the helper to inject HTMX.
Enhanced convenience helper: enhanced_renderer.render_form_html()¶
File: pydantic_schemaforms/enhanced_renderer.py
include_framework_assets: include framework CSS/JS in the returned HTML (default:False).asset_mode: controls how those assets are emitted.self_contained=True: convenience flag equivalent toinclude_framework_assets=Trueandasset_mode="vendored".
Example (simple βjust give me a fully styled Bootstrap formβ):
from pydantic_schemaforms.enhanced_renderer import render_form_html
html = render_form_html(
MyForm,
framework="bootstrap",
self_contained=True,
)
Unlike the legacy wrapper, this helper does not append HTMX/IMask tags; itβs a thin convenience wrapper around EnhancedFormRenderer.
Enhanced renderer: EnhancedFormRenderer¶
File: pydantic_schemaforms/enhanced_renderer.py
include_framework_assets: whether the renderer should include framework CSS/JS.asset_mode: controls whether those assets are vendored inline or pinned CDN URLs.
Example:
from pydantic_schemaforms.enhanced_renderer import EnhancedFormRenderer
renderer = EnhancedFormRenderer(
framework="bootstrap",
include_framework_assets=True,
asset_mode="vendored",
)
html = renderer.render_form_from_model(MyForm)
Modern/builder path: FormBuilder + render_form_page()¶
File: pydantic_schemaforms/integration/builder.py
FormBuilder(..., include_framework_assets=..., asset_mode=...)controls how the builderβs form HTML is rendered.render_form_page(..., include_framework_assets=..., asset_mode=...)controls the full-page wrapperβs CSS/JS emission.
Example:
from pydantic_schemaforms.integration.builder import FormBuilder, render_form_page
builder = FormBuilder(
framework="bootstrap",
include_framework_assets=True,
asset_mode="vendored",
).text_input("ssn", "SSN")
page = render_form_page(
builder,
title="Signup",
include_framework_assets=True,
asset_mode="vendored",
)
Whatβs currently vendored¶
- HTMX
- IMask
- Bootstrap (CSS + bundle JS)
- Materialize (CSS + JS)
See pydantic_schemaforms/assets/vendor/vendor_manifest.json for exact versions and file paths.
Updating vendored assets¶
Vendored updates are scripted and checksum-verified.
- Verify vendored checksums:
-
make vendor-verify -
Update assets:
make vendor-update-htmx HTMX_VERSION=β¦make vendor-update-imask IMASK_VERSION=β¦(or omit to use npm latest)make vendor-update-bootstrap BOOTSTRAP_VERSION=β¦make vendor-update-materialize MATERIALIZE_VERSION=β¦
After updating, run make vendor-verify and the test suite.
Security note¶
asset_mode="cdn" is intentionally available, but it re-introduces an external dependency at runtime. For production systems with strict supply-chain or offline requirements, prefer asset_mode="vendored".
Render Timing¶
The pydantic-schemaforms library automatically measures the time it takes to render forms and provides multiple ways to view this performance data.
Overview¶
Form rendering timing is always measured by the library, giving you insights into form generation performance. You control how and where this timing information is displayed through render-time options.
Display Methods¶
1. Logging (Default)¶
Render time is automatically logged at INFO level to the library's logger:
import logging
from pydantic_schemaforms import render_form_html
# Configure logging to see timing logs
logging.basicConfig(level=logging.INFO)
html = render_form_html(MyForm)
# Output: INFO pydantic_schemaforms.enhanced_renderer: Form rendered in 0.0045s
The logging happens regardless of whether you display timing visually. This means:
- β Timing is always tracked for performance monitoring
- β You can collect timing metrics in production (at INFO level)
- β No performance overhead from timing collection
2. Inline Display (show_timing=True)¶
Add a small timing display below the submit button:
html = render_form_html(MyForm, show_timing=True)
This renders:
<div style="text-align: center; margin-top: 10px; font-size: 0.85rem; color: #666;">
Rendered in 0.0045s
</div>
Use case: During development to see quick feedback on render performance.
3. Debug Panel (debug=True)¶
Include a comprehensive debug panel showing timing and other metadata:
html = render_form_html(MyForm, debug=True)
The debug panel displays in the form header:
Debug panel (development only) β 0.0045s render
And includes additional information: - Form name and model - Number of fields - Framework and layout info - Timing breakdown if available
Use case: Development mode to understand form structure and performance.
4. Combined Display¶
You can use both show_timing and debug simultaneously:
html = render_form_html(MyForm, show_timing=True, debug=True)
This shows timing in: 1. The inline display below submit button 2. The debug panel header 3. The INFO log
Practical Examples¶
Development: See All Timing¶
# Enable debug panel with inline timing
html = render_form_html(
MyForm,
debug=True,
show_timing=True,
framework="bootstrap"
)
Production: Collect Timing Metrics¶
import logging
# Configure logging to collect timing in production
logging.basicConfig(level=logging.INFO)
# Render normally - timing is logged automatically
html = render_form_html(MyForm, framework="bootstrap")
Parse logs to collect performance metrics.
Hide Timing from Users (but Keep Logging)¶
# Users won't see timing, but it's still logged
html = render_form_html(MyForm, framework="bootstrap")
Performance Considerations¶
The timing measurement itself has minimal overhead (typically < 0.1ms). The overhead comes from:
- Calling
time.time()twice (~0.001ms) - negligible - Logging the result (~0.1-1ms if logging is configured) - depends on log handlers
- Rendering the HTML display (~0.01ms) - only if
show_timing=True
In practice, form rendering typically takes 5-50ms depending on form complexity, making timing overhead < 1% of total time.
Integration with Application Logging¶
The timing logs respect your application's logging configuration:
import logging
from pydantic_schemaforms import render_form_html
# Production: INFO level logs
logging.basicConfig(level=logging.INFO)
html = render_form_html(MyForm)
# β
Timing appears in logs
# Development: DEBUG level
logging.basicConfig(level=logging.DEBUG)
html = render_form_html(MyForm)
# β
Timing + detailed library logs appear
# Suppress library logs (but keep timing in code)
logging.getLogger('pydantic_schemaforms').setLevel(logging.WARNING)
html = render_form_html(MyForm)
# β Timing logs suppressed (but timing still measured internally)
FastAPI Example¶
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from pydantic_schemaforms import render_form_html, FormModel, Field
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
app = FastAPI()
class LoginForm(FormModel):
username: str = Field(...)
password: str = Field(..., ui_element="password")
@app.get("/login", response_class=HTMLResponse)
async def login_page(debug: bool = False):
# Toggle timing/debug display via query parameter
html = render_form_html(
LoginForm,
show_timing=debug,
debug=debug,
framework="bootstrap"
)
return f"""
<!doctype html>
<html>
<head><title>Login</title></head>
<body>
<h1>Login</h1>
{html}
<p><small>
<a href="?debug=true">Show timing & debug</a>
</small></p>
</body>
</html>
"""
Visit /login for normal form, /login?debug=true to see timing and debug panel.
Reference¶
Parameters¶
| Parameter | Type | Default | Effect |
|---|---|---|---|
show_timing |
bool |
False |
Display timing below submit button |
debug |
bool |
False |
Show debug panel with timing in header |
enable_logging |
bool |
False |
Enable library DEBUG logging (separate from timing logs) |
Note: Timing is always logged at INFO level, independent of the enable_logging parameter.
Logger Names¶
pydantic_schemaforms.enhanced_renderer- Timing logs and general renderer infopydantic_schemaforms- Root logger for all library components
Timing Accuracy¶
Timing uses Python's time.time() function:
- Precision: microsecond resolution (0.001ms)
- Accuracy: depends on OS clock (typically within 1-5ms on modern systems)
- Example: 0.0045s means 4.5 milliseconds
Troubleshooting¶
Timing Not Appearing¶
Problem: You set show_timing=True but don't see timing in HTML.
Solution: Check that show_timing=True is actually being passed. Common mistakes:
# β Wrong - parameter ignored
html = render_form_html(MyForm)
# β
Correct
html = render_form_html(MyForm, show_timing=True)
Logs Not Appearing¶
Problem: Timing logs don't appear even with logging configured.
Solution: Ensure logging level is at INFO or DEBUG:
import logging
# β WRONG - WARNING level suppresses INFO logs
logging.basicConfig(level=logging.WARNING)
# β
CORRECT - INFO level shows timing
logging.basicConfig(level=logging.INFO)
Performance Unexpectedly Slow¶
Problem: Form rendering takes > 100ms.
Solution: Check form complexity:
- Use
debug=Trueto see form structure - Check if rendering multiple forms in a loop
- Enable DEBUG logging for detailed timing breakdown:
logging.basicConfig(level=logging.DEBUG)
html = render_form_html(MyForm, debug=True)
Logging¶
The pydantic-schemaforms library uses Python's standard logging module to provide visibility into form rendering operations. By default, the library logs at DEBUG level, so logs won't appear in typical production setups.
Overview¶
Logging in pydantic-schemaforms is designed to:
- β Not interfere with your application's logging
- β Provide detailed debug information when needed
- β Use standard Python logging practices
- β Always log timing information (at INFO level, not DEBUG)
Logging Levels¶
Library Logs (DEBUG level)¶
The library uses DEBUG level for detailed information:
import logging
# Configure to see library DEBUG logs
logging.basicConfig(level=logging.DEBUG)
from pydantic_schemaforms import render_form_html
html = render_form_html(MyForm)
# Now you'll see DEBUG logs from the library
Examples of DEBUG logs: - Schema parsing steps - Field rendering decisions - Asset inclusion checks - Layout calculations
Timing Logs (INFO level)¶
Render timing is logged at INFO level, so it appears even in production:
import logging
logging.basicConfig(level=logging.INFO)
html = render_form_html(MyForm)
# Output: INFO pydantic_schemaforms.enhanced_renderer: Form rendered in 0.0045s
This is intentional - timing metrics are valuable for production monitoring while keeping other debug logs suppressed.
Configuration Approaches¶
Approach 1: Application-Level Control (Recommended)¶
Configure your application's logging level. The library respects it:
import logging
# Production: INFO level
logging.basicConfig(level=logging.INFO)
# β
Timing logs appear
# β Library DEBUG logs suppressed
# Development: DEBUG level
logging.basicConfig(level=logging.DEBUG)
# β
Timing logs appear
# β
Library DEBUG logs appear
# Silent: WARNING level
logging.basicConfig(level=logging.WARNING)
# β Timing logs suppressed
# β Library DEBUG logs suppressed
Pros: - Standard Python logging approach - Simple and predictable - Works with all logging handlers
Cons: - Can't selectively enable library logs without enabling all DEBUG logs
Approach 2: Library-Specific Control¶
Enable/disable logging per render call:
from pydantic_schemaforms import render_form_html
# Enable library DEBUG logs for this render
html = render_form_html(MyForm, enable_logging=True)
# Disable library DEBUG logs even if DEBUG level is set
html = render_form_html(MyForm, enable_logging=False)
Pros: - Fine-grained control per call - Can debug specific forms
Cons:
- enable_logging only controls DEBUG level
- enable_logging doesn't affect timing logs (INFO level)
Note: enable_logging is for DEBUG logs. Timing (INFO level) is always logged unless you configure logging levels.
Approach 3: Selective Logger Configuration¶
Enable DEBUG logging only for the library:
import logging
# Application logs at INFO level
logging.basicConfig(level=logging.INFO)
# Library logs at DEBUG level
library_logger = logging.getLogger('pydantic_schemaforms')
library_logger.setLevel(logging.DEBUG)
from pydantic_schemaforms import render_form_html
html = render_form_html(MyForm)
# β
Timing logs appear (INFO)
# β
Library DEBUG logs appear
# β
Other DEBUG logs from your app are suppressed
Pros: - Selective logging without affecting app - Can debug library without app noise
Cons: - More complex setup - Requires understanding logger hierarchy
Practical Scenarios¶
Production Server¶
# At app startup
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Forms render normally
html = render_form_html(MyForm)
# β
Timing appears in logs for monitoring
# β Debug logs suppressed
Local Development¶
# At app startup
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(levelname)s - %(name)s - %(message)s'
)
# See all details
html = render_form_html(MyForm)
# β
Timing logs appear
# β
Detailed library DEBUG logs appear
Development (Want App Logs Only)¶
import logging
# App at DEBUG level, library at WARNING level
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('pydantic_schemaforms').setLevel(logging.WARNING)
html = render_form_html(MyForm)
# β
Your app's DEBUG logs appear
# β Library logs suppressed
Debugging a Specific Form¶
import logging
# Global INFO level
logging.basicConfig(level=logging.INFO)
# Debug this specific form
html = render_form_html(MyForm, enable_logging=True)
# Render other forms normally (no debug logs)
html2 = render_form_html(OtherForm)
FastAPI Integration¶
Example 1: Production Setup¶
from fastapi import FastAPI
import logging
app = FastAPI()
# Configure logging once at startup
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
from pydantic_schemaforms import render_form_html, FormModel, Field
class LoginForm(FormModel):
username: str = Field(...)
password: str = Field(..., ui_element="password")
@app.get("/login")
def login_page():
html = render_form_html(LoginForm, framework="bootstrap")
# β
Timing automatically logged
# β No debug spam
return f"<html><body>{html}</body></html>"
Example 2: Development with Debug Toggle¶
from fastapi import FastAPI, Query
import logging
app = FastAPI()
# Configure at DEBUG for development
logging.basicConfig(level=logging.DEBUG)
from pydantic_schemaforms import render_form_html
@app.get("/login")
def login_page(debug: bool = False):
# Control logging per request
html = render_form_html(
LoginForm,
enable_logging=debug,
debug=debug, # Also show debug panel
framework="bootstrap"
)
return f"<html><body>{html}</body></html>"
# Visit /login for normal form
# Visit /login?debug=true for debug logs + panel
Example 3: Custom Log Handler¶
import logging
from pythonjsonlogger import jsonlogger
# Use JSON logging
logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(logHandler)
logger.setLevel(logging.INFO)
from fastapi import FastAPI
from pydantic_schemaforms import render_form_html
app = FastAPI()
@app.get("/login")
def login_page():
html = render_form_html(LoginForm, framework="bootstrap")
# β
Timing logged as JSON for structured logging
return f"<html><body>{html}</body></html>"
Flask Integration¶
Basic Setup¶
from flask import Flask
import logging
app = Flask(__name__)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
from pydantic_schemaforms import render_form_html
@app.route("/login")
def login():
html = render_form_html(LoginForm, framework="bootstrap")
# β
Timing automatically logged
return f"<html><body>{html}</body></html>"
Conditional Logging¶
import logging
from flask import Flask
app = Flask(__name__)
# Configure based on environment
log_level = logging.DEBUG if app.debug else logging.INFO
logging.basicConfig(level=log_level)
# Can also use Flask's logger
app.logger.setLevel(log_level)
from pydantic_schemaforms import render_form_html
@app.route("/login")
def login():
html = render_form_html(LoginForm, framework="bootstrap")
# β
Respects Flask's log level
return f"<html><body>{html}</body></html>"
Logger Names¶
The library uses these logger names for organization:
import logging
# Main renderer logger - timing and general info
renderer_logger = logging.getLogger('pydantic_schemaforms.enhanced_renderer')
# Root library logger - all components
root_logger = logging.getLogger('pydantic_schemaforms')
# Configure root to affect all components
root_logger.setLevel(logging.DEBUG)
Reference¶
Logging Control Parameters¶
| Parameter | Type | Default | Effect |
|---|---|---|---|
enable_logging |
bool |
False |
Enable DEBUG-level library logs for this render |
Important: enable_logging only affects DEBUG level logs. Timing (INFO level) is always logged and controlled by your application's logging configuration.
Log Levels in Use¶
| Level | Who Controls | Use Case |
|---|---|---|
| INFO | Application logging config | Timing metrics, important events |
| DEBUG | enable_logging param + app config |
Detailed rendering steps, diagnostics |
| WARNING | Application logging config | Important warnings, issues |
Best Practices¶
β DO¶
- Use application-level logging configuration (Approach 1)
- Configure INFO level for production (to see timing)
- Enable DEBUG level during development (to see all details)
- Use standard Python logging handlers and formatters
- Log to structured formats (JSON) in production
β DON'T¶
- Rely on
enable_loggingfor production debugging - Configure library logging differently from your app
- Expect DEBUG logs without DEBUG level configured
- Forget that timing is INFO level (appears in production)
Troubleshooting¶
Logs Not Appearing¶
Problem: Library logs don't appear even with enable_logging=True.
Solution: Check logging level:
import logging
# β WRONG - WARNING suppresses DEBUG
logging.basicConfig(level=logging.WARNING)
html = render_form_html(MyForm, enable_logging=True)
# β
CORRECT - INFO or DEBUG allows logs
logging.basicConfig(level=logging.DEBUG)
html = render_form_html(MyForm, enable_logging=True)
Too Many Logs¶
Problem: Library logs are overwhelming your app logs.
Solution: Configure library logger separately:
import logging
# App at DEBUG
logging.basicConfig(level=logging.DEBUG)
# Library at WARNING (suppress DEBUG logs)
logging.getLogger('pydantic_schemaforms').setLevel(logging.WARNING)
Can't Find Timing in Logs¶
Problem: Timing logs aren't appearing.
Solution: Timing logs at INFO level:
import logging
# β WRONG - WARNING suppresses INFO
logging.basicConfig(level=logging.WARNING)
# β
CORRECT - INFO or DEBUG shows timing
logging.basicConfig(level=logging.INFO)
JSON Logging Not Working¶
Problem: Logs don't serialize to JSON properly.
Solution: Ensure JSON formatter handles all log attributes:
from pythonjsonlogger import jsonlogger
import logging
handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter(
'%(asctime)s %(name)s %(levelname)s %(message)s'
)
handler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.INFO)
Unified Validation Engine Guide¶
Complete guide to validation in pydantic-schemaforms: server-side, real-time HTMX, and cross-field patterns.
Overview¶
The pydantic-schemaforms validation system is consolidated into a single, unified engine that works seamlessly across:
- Server-side validation via validate_form_data() and FormValidator
- Real-time HTMX validation via LiveValidator and field-level validators
- Cross-field validation via form-level rules
- Convenience validators for common patterns (email, password strength)
All validation rules live in pydantic_schemaforms/validation.py, re-exported from pydantic_schemaforms/live_validation.py for convenience, eliminating code duplication and ensuring consistency across all validation flows.
Core Concepts¶
ValidationResponse¶
The canonical response object for all validation operations (server-side or HTMX):
from pydantic_schemaforms import ValidationResponse
response = ValidationResponse(
field_name="email",
is_valid=True,
errors=[], # List of error messages
warnings=[], # List of warnings (non-blocking)
suggestions=["Example: user@example.com"], # Helpful hints
value="user@example.com", # The validated value
formatted_value="user@example.com" # Optionally formatted (e.g., lowercase)
)
# Serialize for HTMX responses
json_str = response.to_json()
dict_response = response.to_dict()
ValidationSchema & FieldValidator¶
Build reusable validation schemas from individual field validators:
from pydantic_schemaforms.validation import ValidationSchema, FieldValidator
# Create a schema with multiple fields
schema = ValidationSchema()
# Add field validators
email_validator = FieldValidator("email")
email_validator.add_rule(EmailRule())
schema.add_field(email_validator)
password_validator = FieldValidator("password")
password_validator.add_rule(
LengthRule(min=8, message="Minimum 8 characters required")
)
schema.add_field(password_validator)
# Build HTMX live validator from schema
live_validator = schema.build_live_validator()
FormValidator¶
Validate entire forms with both field-level and cross-field rules:
from pydantic_schemaforms.validation import FormValidator
form_validator = FormValidator()
# Add field validators
form_validator.field("age").add_rule(NumericRangeRule(min=0, max=150))
form_validator.field("email").add_rule(EmailRule())
# Add cross-field validation
def validate_age_and_consent(data):
age = data.get("age")
consent = data.get("parental_consent")
if age is not None and age < 18 and not consent:
return False, {
"parental_consent": ["Parental consent required for users under 18"]
}
return True, {}
form_validator.add_cross_field_rule(validate_age_and_consent)
# Validate form data
is_valid, errors = form_validator.validate({
"age": 16,
"email": "teen@example.com",
"parental_consent": False
})
Server-Side Validation¶
Using validate_form_data()¶
For simple synchronous validation against a Pydantic FormModel:
from pydantic_schemaforms import FormModel, FormField, validate_form_data
class RegistrationForm(FormModel):
username: str = FormField(
title="Username",
min_length=3,
max_length=20
)
email: str = FormField(
title="Email Address",
input_type="email"
)
password: str = FormField(
title="Password",
input_type="password",
min_length=8
)
# Validate incoming form data
result = validate_form_data(RegistrationForm, {
"username": "alice",
"email": "alice@example.com",
"password": "SecurePass123!"
})
if result.is_valid:
print(f"Valid! Data: {result.data}")
else:
print(f"Invalid! Errors: {result.errors}")
# Result has: result.is_valid, result.data, result.errors
Using FormValidator with Pydantic Models¶
For validation with additional custom rules:
from pydantic_schemaforms.validation import FormValidator
form_validator = FormValidator()
form_validator.field("username").add_rule(LengthRule(min=3, max=20))
form_validator.field("email").add_rule(EmailRule())
form_validator.field("password").add_rule(LengthRule(min=8))
# Validate and get results
is_valid, errors = form_validator.validate({
"username": "alice",
"email": "alice@example.com",
"password": "SecurePass123!"
})
# Also validate against Pydantic model
is_valid, errors = form_validator.validate_pydantic_model(
RegistrationForm,
request_data
)
Real-Time HTMX Validation¶
LiveValidator Setup¶
Use LiveValidator for server-side validation triggered via HTMX on blur/change events:
from pydantic_schemaforms.live_validation import LiveValidator, HTMXValidationConfig
from pydantic_schemaforms.validation import FieldValidator, EmailRule
# Configure HTMX behavior
config = HTMXValidationConfig(
validate_on_blur=True, # Validate when field loses focus
validate_on_input=False, # Don't validate on every keystroke
validate_on_change=True, # Validate when value changes
debounce_ms=300, # Wait 300ms before validation request
show_success_indicators=True, # Visual feedback on valid input
show_warnings=True, # Display warnings
show_suggestions=True, # Show helpful hints
success_class="is-valid", # Bootstrap/custom CSS classes
error_class="is-invalid",
warning_class="has-warning",
loading_class="is-validating"
)
live_validator = LiveValidator(config)
# Register field validators
email_validator = FieldValidator("email")
email_validator.add_rule(EmailRule())
live_validator.register_field_validator(email_validator)
password_validator = FieldValidator("password")
password_validator.add_rule(LengthRule(min=8))
live_validator.register_field_validator(password_validator)
HTML Integration with HTMX¶
In your template, set up HTMX triggers for real-time validation:
<!-- Form field with HTMX validation -->
<input
type="email"
name="email"
id="email"
class="form-control"
placeholder="you@example.com"
hx-post="/validate/email"
hx-trigger="blur, change delay:300ms"
hx-target="#email-feedback"
hx-swap="outerHTML"
/>
<!-- Validation feedback container -->
<div id="email-feedback"></div>
FastAPI Endpoint for HTMX Validation¶
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from pydantic_schemaforms.live_validation import LiveValidator
from pydantic_schemaforms.validation import FieldValidator, EmailRule
app = FastAPI()
live_validator = LiveValidator()
# Register validators
email_validator = FieldValidator("email")
email_validator.add_rule(EmailRule())
live_validator.register_field_validator(email_validator)
@app.post("/validate/email", response_class=HTMLResponse)
async def validate_email(request: Request):
data = await request.form()
value = data.get("email", "")
# Get validator for this field
validator = live_validator.get_field_validator("email")
response = validator.validate(value)
# Render feedback HTML
if response.is_valid:
return f"""
<div id="email-feedback" class="valid-feedback">
β Email looks good
</div>
"""
else:
errors_html = "".join([f"<li>{e}</li>" for e in response.errors])
return f"""
<div id="email-feedback" class="invalid-feedback">
<ul>{errors_html}</ul>
</div>
"""
Building LiveValidator from ValidationSchema¶
Automatically convert a schema to HTMX-ready validators:
from pydantic_schemaforms.validation import ValidationSchema, FieldValidator, EmailRule
schema = ValidationSchema()
email_validator = FieldValidator("email")
email_validator.add_rule(EmailRule())
schema.add_field(email_validator)
# Create HTMX live validator from schema
live_validator = schema.build_live_validator()
# Now use live_validator in HTMX endpoints
Cross-Field Validation¶
Form-Level Rules¶
Validate fields that depend on other fields:
from pydantic_schemaforms.validation import FormValidator
form_validator = FormValidator()
# Individual field rules
form_validator.field("age").add_rule(NumericRangeRule(min=0, max=150))
form_validator.field("parental_consent").add_rule(RequiredRule())
# Cross-field validation
def validate_minor_consent(data):
"""Minors must have parental consent."""
age = data.get("age")
consent = data.get("parental_consent")
if age is not None and age < 18 and not consent:
return False, {
"parental_consent": [
"Parental consent is required for users under 18 years old"
]
}
return True, {}
form_validator.add_cross_field_rule(validate_minor_consent)
# Validate returns both field and cross-field errors
is_valid, errors = form_validator.validate({
"age": 16,
"parental_consent": False
})
# errors = {"parental_consent": ["Parental consent is required..."]}
Conditional Field Validation¶
Validate a field only if another field has a certain value:
def validate_emergency_contact(data):
"""Emergency contact required if no direct phone provided."""
has_phone = bool(data.get("phone"))
has_emergency_contact = bool(data.get("emergency_contact"))
if not has_phone and not has_emergency_contact:
return False, {
"emergency_contact": [
"Either a phone number or emergency contact is required"
]
}
return True, {}
form_validator.add_cross_field_rule(validate_emergency_contact)
Password Matching Validation¶
def validate_passwords_match(data):
"""Ensure password and confirm_password match."""
password = data.get("password", "")
confirm = data.get("confirm_password", "")
if password and confirm and password != confirm:
return False, {
"confirm_password": ["Passwords do not match"]
}
return True, {}
form_validator.add_cross_field_rule(validate_passwords_match)
Convenience Validators¶
Email Validator¶
from pydantic_schemaforms.validation import create_email_validator
email_validator = create_email_validator()
response = email_validator("user@example.com")
# ValidationResponse(field_name="email", is_valid=True, ...)
response = email_validator("invalid-email")
# ValidationResponse(
# field_name="email",
# is_valid=False,
# errors=["Please enter a valid email address"],
# suggestions=["Example: user@example.com"],
# value="invalid-email"
# )
Password Strength Validator¶
from pydantic_schemaforms.validation import create_password_strength_validator
password_validator = create_password_strength_validator(min_length=8)
response = password_validator("WeakPass")
# ValidationResponse(
# field_name="password",
# is_valid=False,
# errors=["Password must be at least 8 characters long"],
# warnings=[
# "Password should contain at least one uppercase letter",
# "Password should contain at least one number"
# ],
# suggestions=[
# "Add an uppercase letter (A-Z)",
# "Add a number (0-9)"
# ],
# value="WeakPass"
# )
response = password_validator("SecurePass123!")
# ValidationResponse(field_name="password", is_valid=True, ...)
Common Validation Rules¶
Built-in Rules¶
The validation system includes pre-built rules for common patterns:
| Rule | Purpose | Example |
|---|---|---|
RequiredRule() |
Field must have a value | Required name field |
LengthRule(min, max) |
String length constraints | 3β20 char username |
EmailRule() |
Valid email format | Email field |
PhoneRule() |
Valid phone number | Phone field |
NumericRangeRule(min, max) |
Numeric value range | Age 0β150 |
DateRangeRule(min_date, max_date) |
Date within range | Future date only |
RegexRule(pattern) |
Custom regex pattern | Custom format validation |
CustomRule(func) |
Custom validation function | Complex logic |
Example: Complete Field Validation¶
from pydantic_schemaforms.validation import (
FieldValidator,
EmailRule,
LengthRule,
NumericRangeRule
)
# Email field validator
email_validator = FieldValidator("email")
email_validator.add_rule(RequiredRule("Email is required"))
email_validator.add_rule(EmailRule())
# Username field validator
username_validator = FieldValidator("username")
username_validator.add_rule(RequiredRule("Username is required"))
username_validator.add_rule(LengthRule(min=3, max=20, message="3β20 characters"))
# Age field validator
age_validator = FieldValidator("age")
age_validator.add_rule(NumericRangeRule(min=13, max=150, message="Must be 13+"))
# Use in form validator
form_validator = FormValidator()
form_validator.field("email").add_rule(EmailRule())
form_validator.field("username").add_rule(LengthRule(min=3, max=20))
form_validator.field("age").add_rule(NumericRangeRule(min=13, max=150))
Sync + HTMX Validation Flow¶
End-to-End Example¶
Here's a complete registration form with both server validation and real-time HTMX feedback:
1. Define Form Model¶
from pydantic_schemaforms import FormModel, FormField
class RegistrationForm(FormModel):
username: str = FormField(
title="Username",
input_type="text",
min_length=3,
max_length=20,
help_text="3β20 alphanumeric characters"
)
email: str = FormField(
title="Email Address",
input_type="email",
help_text="We'll send a confirmation link"
)
password: str = FormField(
title="Password",
input_type="password",
min_length=8,
help_text="Must be at least 8 characters"
)
confirm_password: str = FormField(
title="Confirm Password",
input_type="password",
help_text="Re-enter your password"
)
age: int = FormField(
title="Age",
input_type="number",
ge=13,
le=150,
help_text="Must be 13 or older"
)
2. Set Up Validation¶
from pydantic_schemaforms.validation import (
FormValidator,
FieldValidator,
EmailRule,
LengthRule,
NumericRangeRule
)
# Create form validator with all rules
form_validator = FormValidator()
# Field validators
form_validator.field("username").add_rule(
LengthRule(min=3, max=20, message="3β20 characters")
)
form_validator.field("email").add_rule(EmailRule())
form_validator.field("password").add_rule(
LengthRule(min=8, message="Minimum 8 characters")
)
form_validator.field("age").add_rule(
NumericRangeRule(min=13, max=150, message="Must be 13+")
)
# Cross-field rules
def validate_passwords_match(data):
if data.get("password") != data.get("confirm_password"):
return False, {"confirm_password": ["Passwords do not match"]}
return True, {}
form_validator.add_cross_field_rule(validate_passwords_match)
# Live validator for HTMX
live_validator = form_validator.build_live_validator()
3. FastAPI Endpoints¶
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic_schemaforms import render_form, validate_form_data
app = FastAPI()
@app.get("/register")
def show_registration():
form = RegistrationForm()
return render_form(form, framework="bootstrap", submit_url="/register")
@app.post("/register")
async def handle_registration(request: Request):
# Get form data
form_data = await request.form()
# Server-side validation
result = validate_form_data(RegistrationForm, dict(form_data))
if result.is_valid:
# Process registration
return JSONResponse({
"success": True,
"message": "Registration successful!"
})
else:
# Return form with errors
form = RegistrationForm()
return render_form(
form,
framework="bootstrap",
errors=result.errors,
submit_url="/register"
)
# HTMX validation endpoints
@app.post("/validate/username")
async def validate_username(request: Request):
data = await request.form()
value = data.get("username", "")
validator = form_validator.field("username")
response = validator.validate(value)
if response.is_valid:
return HTMLResponse(
f'<div class="valid-feedback">β Available</div>'
)
else:
errors = "".join([f"<li>{e}</li>" for e in response.errors])
return HTMLResponse(
f'<div class="invalid-feedback"><ul>{errors}</ul></div>'
)
@app.post("/validate/email")
async def validate_email(request: Request):
data = await request.form()
value = data.get("email", "")
response = form_validator.field("email").validate(value)
if response.is_valid:
return HTMLResponse(
f'<div class="valid-feedback">β Valid email</div>'
)
else:
errors = "".join([f"<li>{e}</li>" for e in response.errors])
return HTMLResponse(
f'<div class="invalid-feedback"><ul>{errors}</ul></div>'
)
4. HTML Template¶
<form hx-post="/register" hx-target="#form-result">
<!-- Username field with HTMX validation -->
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
class="form-control"
placeholder="3β20 characters"
hx-post="/validate/username"
hx-trigger="blur, change delay:300ms"
hx-target="#username-feedback"
hx-swap="outerHTML"
/>
<div id="username-feedback"></div>
</div>
<!-- Email field with HTMX validation -->
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
class="form-control"
placeholder="you@example.com"
hx-post="/validate/email"
hx-trigger="blur, change delay:300ms"
hx-target="#email-feedback"
hx-swap="outerHTML"
/>
<div id="email-feedback"></div>
</div>
<!-- Other fields... -->
<button type="submit" class="btn btn-primary">Register</button>
<div id="form-result"></div>
</form>
Testing Your Validators¶
The test suite includes comprehensive coverage. Use these patterns in your tests:
import pytest
from pydantic_schemaforms.validation import (
FormValidator,
FieldValidator,
EmailRule,
ValidationResponse
)
def test_email_validation():
email_validator = FieldValidator("email")
email_validator.add_rule(EmailRule())
# Valid email
response = email_validator.validate("user@example.com")
assert response.is_valid
assert response.errors == []
# Invalid email
response = email_validator.validate("not-an-email")
assert not response.is_valid
assert len(response.errors) > 0
def test_cross_field_validation():
form_validator = FormValidator()
def validate_passwords(data):
if data.get("password") != data.get("confirm"):
return False, {"confirm": ["Passwords don't match"]}
return True, {}
form_validator.add_cross_field_rule(validate_passwords)
is_valid, errors = form_validator.validate({
"password": "secret",
"confirm": "different"
})
assert not is_valid
assert "confirm" in errors
Layout Demo & Tab Rendering Verification¶
The tests/test_layout_demo_smoke.py smoke test verifies that initial tab content renders correctly for both Bootstrap and Material frameworks:
def test_layout_demo_bootstrap_initial_tab_renders():
"""Verify Bootstrap tabs show initial tab content."""
response = client.get("/layouts")
assert response.status_code == 200
assert "Tab 1 Content" in response.text
# Assert tab buttons exist
assert 'class="nav-link active"' in response.text
def test_layout_demo_material_initial_tab_renders():
"""Verify Material tabs show initial tab content."""
response = client.get("/layouts?style=material")
assert response.status_code == 200
# Assert initial content and Material tab classes
assert "Initial Tab Content" in response.text
assert 'data-toggle="tab"' in response.text
This coverage ensures that tab layouts work correctly across frameworks.
Pydantic v2 Deprecation Resolution¶
As of this release, all Pydantic v2 deprecation warnings have been resolved:
β
Resolved Deprecations:
- min_items/max_items β min_length/max_length in all FormField calls
- Extra kwargs on Field() β properly use json_schema_extra
- Starlette TemplateResponse signature updated to new parameter order
Result: Deprecation warnings reduced from 23 β 8 (removed 15 Pydantic deprecations). The remaining 8 warnings are intentional migration guides (form_layouts deprecation notice) and informational (JSON schema hints).
Run validation tests:
python -m pytest tests/test_validation_consolidation.py -v
python -m pytest tests/test_layout_demo_smoke.py -v
Summary¶
The unified validation engine provides:
- Canonical ValidationResponse for all validation flows
- Single code path via
validation.pywith re-exports fromlive_validation.py - Flexible rule composition via
FieldValidatorandFormValidator - HTMX integration via
LiveValidatorwith configurable behavior - Cross-field validation for dependent fields and complex rules
- Convenience validators for common patterns (email, password strength)
- Full async support for FastAPI and async frameworks
- Pydantic v2 compatibility with zero deprecation warnings in critical paths
For questions or examples, see:
- tests/test_validation_consolidation.py β Consolidated validation tests (10 tests)
- tests/test_layout_demo_smoke.py β Layout/tab rendering verification
- examples/fastapi_example.py β Real-world FastAPI integration
Plugin Hooks for Inputs and Layouts¶
This project now exposes lightweight extension points so third-party packages (or your own app code) can add new inputs or layout renderers without patching core modules.
Registering Custom Inputs¶
Use pydantic_schemaforms.inputs.registry.register_input_class to bind a BaseInput subclass to one or more ui_element aliases. The registry augments the built-in discovery of subclasses so you can register at import time or inside your framework startup.
from pydantic_schemaforms.inputs.base import BaseInput
from pydantic_schemaforms.inputs.registry import register_input_class
class ColorSwatchInput(BaseInput):
ui_element = "color_swatch"
# implement render_input / render_label, etc.
register_input_class(ColorSwatchInput)
- Aliases: pass
aliases=("color", "swatch")if you want multiple trigger names. - Bulk registration: use
register_inputs([Cls1, Cls2, ...]). - Reset (tests/hot reload):
reset_input_registry()clears custom entries and cache.
Once registered, any field with input_type="color_swatch" (or alias) will resolve to your component.
Registering Custom Layout Renderers¶
LayoutEngine can now dispatch layout fields to custom renderers before falling back to built-in demos. Provide a callable and reference it from a field via layout_handler (or layout_renderer) in json_schema_extra / FormField kwargs.
from pydantic_schemaforms.rendering.layout_engine import LayoutEngine
# signature: (field_name, field_schema, value, ui_info, context, engine) -> str
def render_steps(field_name, field_schema, value, ui_info, context, engine):
# value may be your own layout descriptor; use engine._renderer if needed
steps = value or []
items = "".join(f"<li>{step}</li>" for step in steps)
return f"<ol class='steps'>{items}</ol>"
LayoutEngine.register_layout_renderer("steps", render_steps)
Attach the handler in your form field:
class WizardForm(FormModel):
steps: list[str] = FormField(
["Account", "Billing", "Review"],
input_type="layout",
layout_handler="steps",
title="Wizard Steps",
)
- Names are arbitrary strings; collisions overwrite the previous handler.
- Call
LayoutEngine.reset_layout_renderers()in tests to clear state. - Handlers receive the active
LayoutEngineinstance and the original renderer viaengine._rendererif you need to reuse field rendering helpers.
Packaging Tips¶
- For libraries: register your inputs/layouts in your package
__init__or an explicitsetup()function that users call during startup. - For app code: register once at process start (e.g., FastAPI lifespan, Django AppConfig.ready). Avoid per-request registration.
- Keep renderers pure and side-effect free; they should return HTML strings and not mutate shared state.
Development
Testing & Contribution Workflow¶
This document describes the authoritative workflow for running tests, linting, and contributing to pydantic-schemaforms.
Single Entry Point: make tests¶
Use make tests (or make test) as the single, canonical command for running all quality checks before committing:
make tests
This runs:
1. Pre-commit hooks (Ruff linting + formatting, YAML/TOML checks, trailing whitespace)
2. Parser regression suite (tests/test_form_data_parsing.py)
3. Full pytest suite (900+ tests covering validation, rendering, async, layouts, integration)
4. Coverage badge generation (summarizes test coverage)
What Changed¶
- Ruff is now enabled in
.pre-commit-config.yamland runs as part ofmake tests - Pre-commit hooks are mandatory before pytest runs; if linting fails, tests don't start
- No manual ruff invocation needed β it's automatic via pre-commit
Workflow¶
Before Committing¶
# Full quality check
make tests
# Or run individual steps if debugging:
make ruff # Lint + format only
make isort # Sort imports
make cleanup # Run all formatters (isort + ruff + autoflake)
What Happens in make tests¶
π Running pre-commit (ruff, formatting, yaml/toml checks)...
β
Pre-commit passed. Running form-data parser regression tests...
π§ͺ Regression passed. Running full pytest suite...
[900+ tests run]
π Generating coverage and test badges...
β¨ Tests complete. Badges updated.
Test Organization¶
Tests are organized in tests/ directory:
| Test File | Purpose | Count |
|---|---|---|
test_layout_demo_smoke.py |
Tab/accordion initial render | 3 |
test_e2e_layouts_async.py |
E2E: structure, integration, async | 14 |
test_plugin_hooks.py |
Input/layout registration | 2 |
test_model_list_integration.py |
Model list rendering & validation | 4 |
test_validation_consolidation.py |
Validation engine unification | 10 |
test_layouts.py |
Layout classes (tabs, accordions, cards) | 35+ |
| ... | Other units, integration, fixtures | 800+ |
Linting Rules (Ruff)¶
Ruff checks for:
- Import ordering (from __future__ first, stdlib, third-party, local)
- Unused imports and variables
- Deprecated patterns (old Pydantic v1 syntax)
- Style (line length, unused code)
- Type hints (basic checks)
If Ruff finds issues, run:
make ruff # Auto-fix what it can
Then manually review any remaining issues and re-run make tests.
CI/CD Integration¶
- Local:
make testsvalidates code before pushing - GitHub Actions (
.github/workflows/testing.yml): Runs same checks on each PR - Pre-commit: Enabled for all developers via
.pre-commit-config.yaml
Dependencies¶
All test dependencies are in requirements.txt:
pip install -r requirements.txt
Key packages:
- pytest + pytest-asyncio + pytest-cov (testing)
- ruff (linting + formatting)
- isort (import sorting)
- genbadge (coverage reporting)
- pre-commit (hook management)
Troubleshooting¶
"pre-commit not found"¶
pip install pre-commit
# or
make install
"pytest not found"¶
pip install -r requirements.txt
Ruff keeps failing on the same file¶
Check what Ruff found:
make ruff
If it's an actual issue (not auto-fixable), edit the file manually and re-run.
Running only pytest (skip pre-commit)¶
pytest tests/
β οΈ Not recommended β linting failures will catch issues in CI anyway.
Running a specific test¶
pytest tests/test_validation_consolidation.py -v
pytest tests/test_e2e_layouts_async.py::TestAsyncFormRendering::test_async_render_returns_same_html_as_sync -v
Contributing¶
- Create a branch:
git checkout -b feature/my-feature - Make changes, add tests
- Run
make teststo validate - Commit and push
- Open a PR β CI will run the same checks
All PRs must pass: - β Ruff linting - β Import ordering (isort) - β pytest (all tests) - β Coverage thresholds
Contributing¶
Please feel free to contribute to this project. Adding common functions is the intent and if you have one to add or improve an existing it is greatly appreciated.
Ways to Contribute¶
- Add or improve a function
- Add or improve documentation
- Add or improve tests
- Report or fix a bug
Testing Workflow¶
- Run
make testsbefore opening or updating a pull request. tests/test_form_data_parsing.pyis a required regression suite and is run explicitly in localmake testsand CI workflows.- Keep this suite passing when changing nested form parsing or assignment behavior.
Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
Unreleased (since untagged-bfb3bb98aa31ca386d88)¶
Breaking Changes¶
submit_urlis now explicit across public form render APIs and no longer defaults silently.- Calls that omit
submit_urlnow raise a clear error, enforcing app-owned routing boundaries.
Added¶
- Async-first rendering flow improvements and examples.
- FastAPI form/style GET+POST matrix testing to validate route and style combinations.
- Expanded coverage suites for renderer/input modules and validation pathways.
Changed¶
- Layout rendering internals refactored for maintainability and reduced cognitive complexity.
- Reliability and maintainability improvements to satisfy SonarCloud findings.
- Improved nested/collapsible form behavior when multiple forms are rendered on a page.
- Better timing/logging support in examples and diagnostics.
Fixed¶
- Removed implicit default submit target behavior (
/submit) and aligned all call sites/tests. - Fixed FastAPI showcase POST re-render path by passing explicit submit target in error flows.
- Fixed datetime/month/week conversion edge cases (
datetimevsdatebranch ordering). - Fixed model-list and nested rendering edge behavior across schema and runtime paths.
Dependencies¶
fastapi[all]:0.121.2β0.128.4ruff:0.14.14β0.15.0tqdm:4.67.1β4.67.3pytest-html:4.1.1β4.2.0packaging:25.0β26.0black:25.12.0β26.1.0mkdocstrings[python]:1.0.0β1.0.1pymdown-extensions:10.20β10.20.1
Latest Changes¶
Removing External Validation Logic (26.1.8.beta)¶
Changes¶
Bug Fixes¶
- Removing validation logic outside of library. (#71) (@devsetgo)
Contributors¶
@devsetgo
Published Date: 2026 February 27, 21:32
Bug fix and cleanup. (26.1.7.beta)¶
Changes¶
- Move of examples (#69) (@devsetgo)
Bug Fixes¶
- fix(layouts): validate nested layout submissions and restore Preferen⦠(#70) (@devsetgo)
Contributors¶
@devsetgo
Published Date: 2026 February 22, 19:56
Many fixes, enhancements, and increasing code coverage (26.1.6.beta)¶
Changes¶
- bumping version to 26.1.6.beta (#68) (@devsetgo)
- feat(examples,docs): add plain HTML style support and document render⦠(#67) (@devsetgo)
- improving documents (#59) (@devsetgo)
- improving test coverage (#58) (@devsetgo)
- improving reliability rating (#57) (@devsetgo)
- working on sonarcloud issue (#41) (@devsetgo)
- fixing examples (#33) (@devsetgo)
Features¶
- working on example. (#65) (@devsetgo)
- Improvements - docs, examples, and bug fixes (#43) (@devsetgo)
- 34 add timing (#42) (@devsetgo)
- adding start line and end line for HTML sent to browser (#32) (@devsetgo)
Bug Fixes¶
- working on material design render issues (#66) (@devsetgo)
- working on example. (#65) (@devsetgo)
- Improvements - docs, examples, and bug fixes (#43) (@devsetgo)
Maintenance¶
- pip(deps): bump tox from 4.36.0 to 4.44.0 (#60) (@dependabot[bot])
- pip(deps): bump mkdocs-material from 9.7.1 to 9.7.2 (#61) (@dependabot[bot])
- pip(deps): bump isort from 7.0.0 to 8.0.0 (#62) (@dependabot[bot])
- pip(deps): bump flask from 3.1.2 to 3.1.3 (#63) (@dependabot[bot])
- pip(deps): bump autoflake from 2.3.1 to 2.3.3 (#64) (@dependabot[bot])
- pip(deps): bump pytest-html from 4.1.1 to 4.2.0 (#45) (@dependabot[bot])
- pip(deps): bump ruff from 0.14.14 to 0.15.0 (#44) (@dependabot[bot])
- pip(deps): bump tqdm from 4.67.1 to 4.67.3 (#47) (@dependabot[bot])
- pip(deps): bump fastapi[all] from 0.121.2 to 0.128.4 (#48) (@dependabot[bot])
- pip(deps): bump mkdocstrings[python] from 1.0.0 to 1.0.1 (#39) (@dependabot[bot])
- pip(deps): bump pymdown-extensions from 10.20 to 10.20.1 (#36) (@dependabot[bot])
- github-actions(deps): bump release-drafter/release-drafter from 6.1.0 to 6.2.0 (#35) (@dependabot[bot])
- pip(deps): bump black from 25.12.0 to 26.1.0 (#37) (@dependabot[bot])
- pip(deps): bump ruff from 0.14.11 to 0.14.14 (#38) (@dependabot[bot])
- pip(deps): bump packaging from 25.0 to 26.0 (#40) (@dependabot[bot])
Contributors¶
@dependabot[bot], @devsetgo and dependabot[bot]
Published Date: 2026 February 22, 16:35
Additional Work for Self-Contained (untagged-bfb3bb98aa31ca386d88)¶
Changes¶
Features¶
- Fix of self-contained and documentation updates (#31) (@devsetgo)
Bug Fixes¶
- Fix of self-contained and documentation updates (#31) (@devsetgo)
- deploy docs (#28) (@devsetgo)
- working on documentation flow bugs (#27) (@devsetgo)
- working on issue to fix publishing failure (#26) (@devsetgo)
Contributors¶
@devsetgo
Published Date: 2026 January 18, 13:57
Bug fixes and Improvements (26.1.2.beta)¶
Changes¶
- Publishing Improvements (#17) (@devsetgo)
- Doc (#16) (@devsetgo)
- GitHub Actions Improvements (#10) (@devsetgo)
- first release (#9) (@devsetgo)
- working on documentation (#5) (@devsetgo)
- working on coverage (#4) (@devsetgo)
Features¶
- Improving Workflow (#1) (@devsetgo)
Bug Fixes¶
- 18 bootstrap not included in self contained example (#24) (@devsetgo)
- Fix model-list delete for dynamically added items (#23) (@devsetgo)
- working on coverage issue (#3) (@devsetgo)
- working on publishing issue (#2) (@devsetgo)
Maintenance¶
- github-actions(deps): bump actions/upload-pages-artifact from 3 to 4 (#19) (@dependabot[bot])
- updating release drafter (#11) (@devsetgo)
- Pre-Release Checks (#6) (@devsetgo)
Contributors¶
@dependabot[bot], @devsetgo and dependabot[bot]
Published Date: 2026 January 09, 21:46
Initial Beta Release (26.1.1.beta)¶
Changes¶
- GitHub Actions Improvements (#10) (@devsetgo)
- first release (#9) (@devsetgo)
- working on documentation (#5) (@devsetgo)
- working on coverage (#4) (@devsetgo)
Features¶
- Improving Workflow (#1) (@devsetgo)
Bug Fixes¶
- working on coverage issue (#3) (@devsetgo)
- working on publishing issue (#2) (@devsetgo)
Maintenance¶
- updating release drafter (#11) (@devsetgo)
- Pre-Release Checks (#6) (@devsetgo)
Contributors¶
@devsetgo
Published Date: 2026 January 02, 19:13
Library Boundary & Security Risk Analysis¶
Context¶
Your stated rule is:
- The library renders and validates forms.
- The application owns endpoint routing and submission destinations.
- The library must not make app-specific assumptions.
This is aligned with secure framework design:
- App layer decides trust boundaries (routes, auth, tenancy, CSRF policy).
- Library layer stays generic (rendering, schema parsing, model validation).
Issue 1: App-specific special case in core¶
Location: pydantic_schemaforms/rendering/layout_engine.py (field_name == "comprehensive_tabs" branch)
Why this is risky¶
The core library now contains behavior tied to one example form field name (comprehensive_tabs).
That causes:
- Coupling to demo/app internals: core behavior depends on one specific field naming convention.
- Hidden precedence rules: data can be reshaped unexpectedly when that key appears.
- Non-obvious behavior for consumers: users with similarly named fields may get unintended transformations.
- Maintenance drift: adding more app-specific exceptions scales badly and becomes fragile.
Security angle¶
This is more of an architectural boundary risk than a direct exploit by itself.
However, implicit reshaping in core can become security-relevant when:
- validation and authorization logic in apps assumes a different input shape,
- field-level allowlists/denylists are bypassed by transformation order,
- audit logs capture transformed payloads that no longer match incoming data.
Recommended direction¶
Remove hardcoded field-name logic from core and replace with one of:
- Schema-driven generic resolution (preferred): detect layout fields by metadata (
ui_element/layout) and resolve nested values generically. - Pluggable hook: app can register a resolver for custom field mapping.
- No implicit mapping: require input to already match model shape; apps perform mapping explicitly.
Decision tradeoff¶
- Generic schema-driven core gives best developer experience with minimal policy risk.
- Explicit app mapping gives strongest predictability and least surprise.
Issue 2: Preserving unknown submitted keys in validated output¶
Location: pydantic_schemaforms/validation.py around unknown-key preservation
Why this is risky¶
The current behavior appends keys from raw input if they are not part of validated model output.
This can leak unvalidated transport fields into the final result.data payload.
Concrete failure modes¶
- Data contamination
- Output looks validated but includes non-model keys.
-
Downstream code may trust all keys in the payload.
-
Privilege/flag smuggling
- Attackers can include fields like
is_admin,role,tenant_id,approved, etc. -
Even if model ignores them, preservation reintroduces them into the final payload.
-
Mass assignment risk in follow-on layers
-
If downstream persistence uses broad dict writes, unknown keys may be persisted or acted upon.
-
Audit ambiguity
- βValidated dataβ is no longer strictly validated model data.
Security severity¶
This is a real security concern in multi-layer systems where consumers assume validated output is safe.
Severity depends on what downstream does, but the default should be conservative.
Recommended direction¶
Default policy should be:
- Return only model-shaped validated data.
- Do not merge unknown keys into validated output.
If compatibility is needed, use explicit opt-in modes:
strict(default): model-only output.include_raw_extra(opt-in): include unknowns under a separate namespace, e.g._extra.
This preserves debuggability without contaminating trusted data.
Related Principle: Submit destination ownership¶
You noted: sending ?style={style} in submit_url is app-owned and acceptable.
Agreed. The key security principle:
- Library may render whatever
submit_urlthe app passes. - Library must not infer or auto-construct submission routes beyond safe defaults.
- Library defaults should be inert (
/submit), explicit, and overridable.
This keeps route ownership with the application and avoids accidental data exfiltration paths.
Suggested policy decisions¶
Choose one policy set and apply consistently:
Option A (recommended)¶
- Remove app-specific hardcoded mappings from core.
- Keep only generic schema-driven layout behavior.
- Return only validated model-shaped output.
- If extras are needed, expose separately (e.g.
_extra) via opt-in.
Option B (compatibility-first)¶
- Keep current behavior behind feature flags.
- Default to secure behavior for new users.
- Offer migration period for legacy apps.
Migration plan (low risk)¶
- Add a validation output policy setting with default
strict. - Deprecate unknown-key merge behavior with warning.
- Remove
comprehensive_tabsspecial case and replace with generic resolver. - Add tests covering:
- no app-specific field-name assumptions,
- unknown key handling under strict/compat modes,
- nested layout payload correctness.
Bottom line¶
Both flagged items are valid concerns.
- The hardcoded
comprehensive_tabsbranch violates library/app separation. - Unknown key preservation can blur trust boundaries and create security risk.
If your goal is a secure, framework-grade library contract, Option A is the safer long-term decision.
Codebase Review β pydantic-schemaforms¶
Date: 2025-12-21 (Updated)
Executive Summary¶
pydantic-schemaforms is in a solid place architecturally: there is now a single schemaβfieldβlayout rendering pipeline, theme/style are first-class and overrideable, and the test suite exercises both sync and async rendering across layout primitives. The remaining work is less about βmore featuresβ and more about locking the product to its original promise:
-
Python 3.14+ only: this library targets Python 3.14 and higher, and does not support earlier Python versions.
-
ship all required HTML/CSS/JS from the library (no external CDNs by default)
- keep UI + validation configuration expressed via Pydantic (schema is the source of truth)
- make sync + async usage boringly simple (one obvious way)
- add an optional debug rendering mode that helps adoption without changing normal UX
The renderer refactor eliminated shared mutable state and restored the enhanced/material renderers to a working baseline. Schema metadata is cached, field rendering is centralized, and model-list nesting now feeds explicit RenderContext objects. Django integration has been removed (Flask/FastAPI remain), and the JSON/OpenAPI generators now source constraints directly from Pydantic field metadata, unblocking the integration tests. Renderer themes now include a formal FrameworkTheme registry (Bootstrap/Material/plain) plus MaterialEmbeddedTheme, and both EnhancedFormRenderer and FieldRenderer source their form/input/button classes from the active theme before falling back to legacy framework config.
Latest (Dec 7, 2025): Theme-driven form chrome extraction is complete. The new FormStyle contract centralizes all framework-specific markup (model lists, tabs, accordions, submit buttons, layout sections, and field-level help/error blocks) in a registry-based system. FormStyleTemplates dataclass now holds 15 template slots (expanded from 13: added field_help and field_error for field-level chrome routing), registered per-framework (Bootstrap/Material/Plain/Default) with graceful fallbacks. RendererTheme and LayoutEngine now query FormStyle.templates at render time instead of inlining markup, enabling runtime overrides without renderer edits. Version-aware style descriptors are supported (e.g., get_form_style("bootstrap:5"), get_form_style("material:3")) with fallback to framework defaults. FastAPI example hardened with absolute paths (Path(__file__).resolve().parent) for templates/static, resolving path issues in tests and different working directories. Validation engine consolidated: ValidationResponse and convenience validators now live in validation.py, live_validation.py consumes/re-exports without duplication, and 10 new consolidation tests were added. Tabs regression fixed: Bootstrap panels now render initial content (show active), Material tabs use shared tab CSS/JS classes so tabs switch correctly, and a layout-demo smoke test asserts initial tab content renders for both frameworks. Pydantic v2 deprecations eliminated: All Pydantic Field() kwargs now properly use json_schema_extra instead of extra kwargs; min_items/max_items replaced with min_length/max_length in FormField calls; Starlette TemplateResponse signature updated to new parameter order (request first). Deprecation warnings suppressed: pytest filterwarnings config reduces test output from 19 warnings to 1 without losing developer guidance (form_layouts deprecation and JSON schema hints remain available with -W default). Validation documentation added: New comprehensive docs/validation_guide.md (787 lines) documents the unified validation engine with ValidationResponse/FieldValidator/FormValidator APIs, server-side and HTMX validation flows, cross-field validation patterns, and end-to-end FastAPI examples.
Latest (Dec 21, 2025): The core architecture is now strong enough to support the original product constraints (library supplies HTML/CSS/JS, configuration is expressed via Pydantic, sync+async are easy). The most important product-alignment work is now complete for assets and consistently enforced:
- Self-contained assets: Default rendering no longer emits external CDN URLs.
- HTMX is now vendored and inlined by default (offline-by-default).
- A CDN mode exists but is explicitly opt-in and pinned to the vendored manifest version.
- IMask is vendored and available when explicitly requested (e.g., SSN masking).
- Framework CSS/JS (Bootstrap + Materialize) are vendored and can be emitted inline in
asset_mode="vendored". - Consistent asset selection: A consistent
asset_modepattern is now threaded through the main entry points (enhanced renderer, modern renderer/builder, legacy wrappers) so βcdn vs vendoredβ behavior is deterministic. - Operational stability:
vendor_manifest.jsonchecksum verification is enforced by tests, and pre-commit is configured to avoid mutating vendored assets and generated test artifacts. - Debug rendering mode (COMPLETED): First-class debug panel now available via
debug=Trueflag. The panel exposes (1) rendered HTML/assets, (2) the Python form/model source, (3) validation rules/schema, and (4) live payload with real-time form data capture. Implementation uses JavaScript event listeners to update the live tab as users interact with the form, handles nested model-list fields correctly, and is fully self-contained (inline CSS/JS). FastAPI example updated with?debug=1support on all routes, and tests verify correct behavior.
Design Rules (NonβNegotiables)¶
These rules are intended to prevent βhelpfulβ drift away from the original concept.
- Python version policy
- The library supports Python 3.14 and higher only.
-
Avoid guidance that suggests compatibility with older Python versions.
-
Library ships the experience
- Default output must be fully functional offline: no third-party CDN assets (JS/CSS/fonts) unless explicitly opted in.
- Framework integrations may serve assets, but the assets must come from this package.
-
Pydantic is the single source of truth
- Validation constraints, required/optional, and shape come from Pydantic schema/Field metadata.
- UI configuration is expressed via Pydantic-friendly metadata (
json_schema_extra/ form field helpers) rather than ad-hoc runtime flags. - Avoid storing non-JSON-serializable objects in schema extras unless they are sanitized for schema generation.
-
One obvious way (sync + async)
- There should be exactly one recommended sync entry point and one async entry point.
- All other helpers should be thin compatibility wrappers and should not diverge in behavior.
-
Renderer outputs deterministic, self-contained HTML
- Rendering should not depend on global mutable state or ambient process configuration.
- Rendering should be deterministic for the same model + config.
-
Debug mode is optional and non-invasive
- Debug UI must be off by default.
- When enabled, it should wrap the existing form (collapsed panel) and never change validation/rendering semantics.
- Debug surfaces should be βread-only viewsβ of: rendered HTML/assets, model source (best effort), schema/validation rules, and live validation payloads.
-
Extensibility stays declarative
- Plugins register inputs/layout renderers via the official registries; no monkeypatching required.
- Extension points should compose with themes/styles, not bypass them.
Critical / High Priority Findings¶
-
External CDN assets violate the self-contained requirement (Addressed) Default output is now offline-by-default across the main entry points.
-
HTMX is vendored and inlined by default.
- An explicit
asset_mode="cdn"exists for users who want CDN delivery, but it is pinned to the vendored manifest version. - IMask is vendored and available for secure inputs (e.g., SSN masking). It is not injected unless explicitly requested.
- Framework CSS/JS (Bootstrap + Materialize) are vendored and can be emitted in
asset_mode="vendored"(inline) to keep βframework lookβ self-contained. - External CDN URLs still exist as an explicit opt-in (
asset_mode="cdn") and are pinned to the vendored manifest versions.
Files:
- pydantic_schemaforms/render_form.py (legacy wrapper)
- pydantic_schemaforms/assets/runtime.py (vendored HTMX + pinned CDN mode)
- pydantic_schemaforms/rendering/themes.py / pydantic_schemaforms/enhanced_renderer.py (asset-mode gating)
- pydantic_schemaforms/modern_renderer.py, pydantic_schemaforms/integration/builder.py (builder/modern entry points)
- pydantic_schemaforms/form_layouts.py (deprecated legacy layouts; now gated)
Vendored dependency policy (implemented for HTMX; extendable for others)
- Default is offline: the default renderer output must not reference external CDNs.
- Pinned + auditable: every vendored asset must be pinned to an explicit version and recorded with source_url + sha256 in a manifest.
- Licenses included: upstream license text (and any required notices) must be included in the repo alongside the vendored asset (or clearly referenced if inclusion is not permitted).
- Single update path: updates must happen via an explicit script + make target (no manual copy/paste), so diffs are reproducible.
- Opt-in CDN mode only: if a CDN mode exists, it must be explicitly selected (never default) and clearly documented as not-offline.
What βeasy to updateβ means (definition)
- One command updates the pinned version, downloads the asset(s), writes/updates the manifest checksums, and runs a verification check.
- A CI/test check fails if any default render output contains external asset URLs.
- The update process is deterministic and reviewable (diff shows only asset bytes + manifest/version bump).
- Formatting/lint tooling must not modify vendored bytes (otherwise checksum verification breaks). Pre-commit should exclude pydantic_schemaforms/assets/vendor/ from whitespace/EOF normalization hooks.
-
Multiple layout stacks compete for ownership (Resolved) Layout definitions now live exclusively in
layout_base.BaseLayout+rendering/layout_engine. The newLayoutComposerAPI exposes the canonical Horizontal/Vertical/Tabbed primitives, whilepydantic_schemaforms.layouts/pydantic_schemaforms.form_layoutsonly re-export the engine withDeprecationWarnings. Enhanced and Material renderers both call intoLayoutEngine, so markup is consistent across frameworks, and the tutorial documents LayoutComposer as the single supported API. Files:pydantic_schemaforms/rendering/layout_engine.py,pydantic_schemaforms/layouts.py,pydantic_schemaforms/form_layouts.py,pydantic_schemaforms/simple_material_renderer.py -
Renderer logic duplicated across frameworks (Resolved) Enhanced and Simple Material share the same orchestration pipeline via the new
RendererThemeabstraction andMaterialEmbeddedTheme, eliminating the duplicated CSS/JS scaffolding that previously lived insimple_material_renderer.py. The Modern renderer now builds a temporaryFormModeland hands off toEnhancedFormRenderer, and the redundantPy314Rendereralias has been removed entirely. Framework-specific assets live inRendererThemestrategies, so there is a single schema walk/layout path regardless of entry point. Files:pydantic_schemaforms/enhanced_renderer.py,pydantic_schemaforms/rendering/themes.py,pydantic_schemaforms/simple_material_renderer.py,pydantic_schemaforms/modern_renderer.py -
Integration helpers mix unrelated responsibilities (Addressed) The synchronous/async adapters now live in
pydantic_schemaforms/integration/frameworks/, leaving the rootintegrationpackage to expose only builder/schema utilities by default. The module uses lazy exports so simply importingpydantic_schemaforms.integrationno longer drags in optional framework glue unless those helpers are actually accessed. Files:pydantic_schemaforms/integration/__init__.py,pydantic_schemaforms/integration/frameworks/,pydantic_schemaforms/integration/builder.py
Medium Priority Refactors & Opportunities¶
-
Input component metadata duplicated (Resolved) Input classes now declare their
ui_element(plus optional aliases) and a lightweight registry walks the class hierarchy to expose a mapping.rendering/frameworks.pyimports that registry instead of maintaining its own list, so adding a new component only requires updating the input module where it already lives. Files:pydantic_schemaforms/inputs/base.py,pydantic_schemaforms/inputs/*,pydantic_schemaforms/inputs/registry.py,pydantic_schemaforms/rendering/frameworks.py -
Model list renderer mixes logic with theme markup (Resolved)
ModelListRenderernow delegates both containers and per-item chrome throughRendererThemehooks:render_model_list_container()and the newrender_model_list_item()(with Material/embedded overrides) wrap the renderer-supplied field grid so frameworks own every byte of markup. Bootstrap/Material share the same plumbing, labels/help/errors/add buttons stay in the theme, and tests cover that custom themes can inject their own classes when rendering lists. Files:pydantic_schemaforms/model_list.py,pydantic_schemaforms/rendering/themes.py -
Template engine under-used (Resolved) The new
FormStylecontract (inrendering/form_style.py) extracts all framework-specific markup intoFormStyleTemplatesdataclass with 13 template slots:form_wrapper,tab_layout,tab_button,tab_panel,accordion_layout,accordion_section,layout_section,layout_help,model_list_container,model_list_item,model_list_help,model_list_error, andsubmit_button. Framework-specific bundles (Bootstrap, Material, Plain, Default) are registered in a centralized registry viaregister_form_style()and queried at render time withget_form_style(framework, variant).RendererTheme.render_submit_button(),render_model_list_*()methods andLayoutEnginetab/accordion layouts all delegate toFormStyle.templateswith graceful fallback to defaults, eliminating inline markup strings and enabling runtime overrides. Tests intest_theme_hooks.py(7 tests) verify custom FormStyle templates drive rendering. FastAPI example paths hardened to usePath(__file__).resolve().parentfor templates and static dirs, working correctly from any working directory. Files:pydantic_schemaforms/rendering/form_style.py,pydantic_schemaforms/rendering/themes.py,pydantic_schemaforms/rendering/layout_engine.py,examples/fastapi_example.py,tests/test_theme_hooks.py,tests/test_fastapi_example_smoke.py -
Runtime field registration surfaced (New) Dynamically extending a
FormModelis now supported viaFormModel.register_field(), which wires the newFieldInfointo the schema cache and the validation stack by synthesizing a runtime subclass when necessary. Legacysetattr(MyForm, name, Field(...))still works for rendering, but the helper ensuresvalidate_form_data()and HTMX live validation enforce the same constraints without manual plumbing. Files:pydantic_schemaforms/schema_form.py,pydantic_schemaforms/validation.py,tests/test_integration_workflow.pyTODO: The temporaryDynamicFormRuntimecreated bypydantic.create_model()emits aUserWarningabout shadowing parent attributes. If this becomes noisy, add a localmodel_config = {"ignored_types": ...}or suppress the warning via the helper before rebuilding the runtime model. -
Validation rule duplication (Resolved) Validation is now canonical in
validation.py(rules,ValidationResponse, convenience validators).live_validation.pyconsumes/re-exports without duplicating code. Added consolidation coverage (10 tests) for schema β live validator flow, convenience validators, and serialization. Files:pydantic_schemaforms/validation.py,pydantic_schemaforms/live_validation.py,pydantic_schemaforms/__init__.py,tests/test_validation_consolidation.py -
Input namespace still re-exports everything (Resolved) The root package now exposes inputs via module-level
__getattr__, delegating to a lazy-loading facade inpydantic_schemaforms.inputs. No wildcard imports remain, so importingpydantic_schemaformsdoes not instantiate every widget or template; consumers still getfrom pydantic_schemaforms import TextInputvia the cached attribute. Future work can build on the same facade to document a plugin hook for third-party inputs. Files:pydantic_schemaforms/__init__.py,pydantic_schemaforms/inputs/__init__.py -
Integration facade duplicated across namespaces (Resolved) The canonical sync/async helpers now live only in
integration/adapters.py,integration/sync.py, andintegration/async_support.py. Theintegration.frameworkspackage re-exports those implementations for legacy imports, andFormIntegration.async_integrationwas converted to a@staticmethodso the API is identical in both namespaces. Optional dependencies remain isolated via lazy imports, but there is now exactly one code path for validation + rendering logic. Files:pydantic_schemaforms/integration/__init__.py,pydantic_schemaforms/integration/adapters.py,pydantic_schemaforms/integration/frameworks/* -
Public sync/async βone obvious wayβ (Resolved) Canonical entry points now exist and are exported from the root package:
- Sync:
handle_form() - Async:
handle_form_async()
Legacy helpers (handle_sync_form, handle_async_form, FormIntegration.*) remain as compatibility wrappers.
Files: pydantic_schemaforms/integration/adapters.py, pydantic_schemaforms/integration/__init__.py, pydantic_schemaforms/__init__.py, docs/quickstart.md, tests/test_integration.py
-
Theme/style contract centralized
RendererThemenow includes concreteFrameworkThemesubclasses for Bootstrap/Material/plain plusget_theme_for_framework, and both enhanced + field renderers request classes/assets from the active theme before falling back to legacy configs.FormStyleregistry now handles framework-level templates (including field-level chrome) and supports version-aware descriptors (e.g.,bootstrap:5,material:3) with fallbacks to base framework/default. Files:pydantic_schemaforms/enhanced_renderer.py,pydantic_schemaforms/rendering/themes.py,pydantic_schemaforms/rendering/field_renderer.py,pydantic_schemaforms/rendering/frameworks.py,pydantic_schemaforms/rendering/form_style.py -
Plugin hooks for inputs/layouts (NEW) Input components can now be registered via
register_input_class()andregister_inputs()ininputs/registry.pywith automatic cache invalidation. Layout renderers can be registered viaLayoutEngine.register_layout_renderer()and referenced from form fields usinglayout_handlermetadata. Both APIs support resettable state for testing. Docs pagedocs/plugin_hooks.mdexplains usage and packaging patterns. Files:pydantic_schemaforms/inputs/registry.py,pydantic_schemaforms/rendering/layout_engine.py,docs/plugin_hooks.md,tests/test_plugin_hooks.py
Testing & Tooling Gaps¶
- β
Renderer behavior E2E coverage (COMPLETED) β Added
tests/test_e2e_layouts_async.pywith 14 tests: unit tests for tab/accordion DOM structure, aria attributes, display state; integration tests forLayoutDemonstrationFormwith nested fields and model lists; async equivalence tests. All passing. - β
CI/docs alignment (COMPLETED) β Documented
make testsas single entry point in newdocs/testing_workflow.md(comprehensive guide with test organization, linting rules, CI/CD integration, troubleshooting). Ruff now enabled in.pre-commit-config.yamland enforced as part ofmake testsbefore pytest runs.
Recommended Next Steps¶
- β
Document unified validation engine (COMPLETED) β Created comprehensive
docs/validation_guide.md(787 lines) with: ValidationResponse,FieldValidator,FormValidator, andValidationSchemaAPI documentation- Server-side validation patterns with
validate_form_data()and custom rules - Real-time HTMX validation with
LiveValidatorandHTMXValidationConfig - Cross-field validation examples (age consent, password matching, conditional fields)
- Convenience validators (
create_email_validator(),create_password_strength_validator()) - Complete end-to-end sync + HTMX flow example with FastAPI endpoints and HTML templates
- Testing patterns and Pydantic v2 deprecation resolution notes
-
References to layout-demo smoke test coverage and tab rendering verification
-
β Suppress remaining expected deprecation warnings (COMPLETED) β Added
filterwarningsto[tool.pytest.ini_options]inpyproject.tomlto suppress intentional warnings:form_layoutsdeprecation notice (migration guidance), Pydantic JSON schema non-serializable defaults (informational), and Pydantic extra kwargs deprecation (handled in code). Result: test output reduced from 19 warnings to 1 in normal mode; warnings remain accessible viapytest -W default. -
β Field-level chrome routing (COMPLETED) β Extended
FormStyleto support field-level help/error templating: Addedfield_helpandfield_errortemplates toFormStyleTemplatesdataclass (15 slots total, up from 13), registered framework-specific versions for Bootstrap, Plain, and Material Design. Ready for field renderers to consume these templates; enables consistent field-level chrome across all frameworks without renderer edits. -
β Version-aware style variants (COMPLETED) β
FormStyledescriptors accept framework + variant (e.g.,"bootstrap:5","material:3") with graceful fallbacks to the framework base and default style. Aliases registered for Bootstrap 5 and Material 3 reuse existing templates; lookup stays backward compatible. -
β Extension hooks for inputs/layouts (COMPLETED) β Plugin registration API added:
register_input_class()/register_inputs()ininputs/registrywith cache clearing, andLayoutEngine.register_layout_renderer()with metadata-driven dispatch. Documented indocs/plugin_hooks.mdwith examples and best practices. -
β Automated E2E coverage for layouts/async (COMPLETED) β Added comprehensive
tests/test_e2e_layouts_async.py(14 tests) covering: unit tests for tab/accordion DOM structure, aria attributes, and display state; integration tests forLayoutDemonstrationFormtab/layout field rendering with nested content and model lists; async tests verifyingrender_form_from_model_async()produces identical HTML to sync path, handles errors gracefully, and supports concurrent rendering. All tests passing. -
β CI/docs alignment (COMPLETED) β Documented
make testsas single entry point in newdocs/testing_workflow.md(comprehensive guide with test organization, linting rules, CI/CD integration, troubleshooting). Ruff now enabled in.pre-commit-config.yamland enforced as part ofmake testsbefore pytest runs. All 217+ tests passing with integrated linting. -
β Make asset delivery self-contained (Completed for HTMX + IMask + framework assets) β The default renderer output is offline-by-default.
- β HTMX is vendored and inlined by default; CDN mode is opt-in and pinned.
-
β IMask is vendored (for SSN and other secure input types) and can be included explicitly.
- β
Bootstrap + Materialize CSS/JS are vendored and can be emitted inline via
asset_mode="vendored"when framework assets are requested.
Update workflow (implemented) - Vendoring and verification scripts/targets exist, and tests enforce βno external URLs by defaultβ. -
docs/assets.mddocuments theasset_modecontract and the vendoring workflow. - β
Bootstrap + Materialize CSS/JS are vendored and can be emitted inline via
Tooling note (important)
- Pre-commit should exclude vendored assets and generated artifacts (coverage/test reports, debug HTML) from whitespace/EOF fixers to keep checksum verification and make tests stable.
- β Define one canonical sync + one canonical async entry point (COMPLETED) β Canonical entry points exist and are documented:
- Sync:
handle_form() - Async:
handle_form_async()
Both are exported from pydantic_schemaforms and covered by integration tests.
Files: pydantic_schemaforms/integration/adapters.py, pydantic_schemaforms/__init__.py, docs/quickstart.md, tests/test_integration.py
- β
Add a first-class debug rendering mode (COMPLETED) β Implemented a
debug=Trueoption that wraps the form in a collapsed debug panel with tabs: - Rendered output: raw HTML (including the CSS/JS assets block)
- Form/model source: Python source for the form class (best-effort via
inspect.getsource()) - Schema / validation: schema-derived constraints (field requirements, min/max, regex, etc.)
- Live payload: Real-time form data capture that updates as users type/interact with the form
Implementation details:
- Debug panel is off by default and non-invasive (collapsed <details> element)
- JavaScript event listeners capture input and change events to update live payload
- Handles nested form data (model lists with pets[0].name notation)
- Proper checkbox handling (true/false instead of "on" or missing)
- Tab UI consistent across frameworks using inline styles/scripts
- Tests verify debug panel presence when enabled and absence by default
- FastAPI example updated to expose ?debug=1 query parameter on all form routes
Files: pydantic_schemaforms/enhanced_renderer.py (debug panel builder), pydantic_schemaforms/render_form.py (debug flag forwarding), tests/test_debug_mode.py (2 tests), examples/fastapi_example.py (debug parameter on all routes)
## Codebase Layout (Package Map)
This section documents the git-tracked layout of the pydantic_schemaforms/ package.
Notes:
- Runtime-generated
__pycache__/folders and*.pycfiles are intentionally omitted here (they are not part of the source tree and should not be committed).
### pydantic_schemaforms/ (root package)
pydantic_schemaforms/__init__.pyβ Public package entry point (lazy exports) and top-level API surface.pydantic_schemaforms/enhanced_renderer.pyβ Enhanced renderer pipeline (schema β fields β layout) with sync/async HTML entry points.pydantic_schemaforms/form_field.pyβFormFieldabstraction and higher-level field helpers aligned with the design vision.pydantic_schemaforms/form_layouts.pyβ Legacy layout composition helpers (kept for compatibility; deprecated).pydantic_schemaforms/icon_mapping.pyβ Framework icon mapping helpers (bootstrap/material icon name/class resolution).pydantic_schemaforms/input_types.pyβ Input type constants, default mappings, and validation utilities for selecting HTML input types.pydantic_schemaforms/layout_base.pyβ Shared base layout primitives used by the layout engine and renderer(s).pydantic_schemaforms/layouts.pyβ Deprecated layout wrapper module (compatibility surface).pydantic_schemaforms/live_validation.pyβ HTMX-oriented βlive validationβ plumbing and configuration.pydantic_schemaforms/model_list.pyβ Rendering helpers for repeating/nested model lists.pydantic_schemaforms/modern_renderer.pyβ βModernβ renderer facade backed by the shared enhanced pipeline.pydantic_schemaforms/render_form.pyβ Backwards-compatible rendering wrapper(s) for legacy entry points.pydantic_schemaforms/schema_form.pyβ Pydantic-driven form model primitives (FormModel,Field, validator helpers, validation result types).pydantic_schemaforms/simple_material_renderer.pyβ Minimal Material Design renderer implementation.pydantic_schemaforms/templates.pyβ Python 3.14 template-string based templating helpers used throughout rendering.pydantic_schemaforms/validation.pyβ Canonical validation rules/engine and serializable validation responses.pydantic_schemaforms/vendor_assets.pyβ Vendoring/manifest helper utilities used to manage and verify shipped third-party assets.pydantic_schemaforms/version_check.pyβ Python version checks (enforces Python 3.14+ assumptions like template strings).
### pydantic_schemaforms/assets/ (packaged assets)
pydantic_schemaforms/assets/__init__.pyβ Asset package marker.pydantic_schemaforms/assets/runtime.pyβ Runtime helpers to load/inline assets and emit tags (vendored vs pinned CDN modes).
#### pydantic_schemaforms/assets/vendor/ (vendored thirdβparty assets)
pydantic_schemaforms/assets/vendor/README.mdβ Vendored asset policy and update workflow overview.pydantic_schemaforms/assets/vendor/vendor_manifest.jsonβ Pin list and SHA256 checksums for vendored assets (audit + verification).
##### pydantic_schemaforms/assets/vendor/bootstrap/
pydantic_schemaforms/assets/vendor/bootstrap/bootstrap.min.cssβ Vendored, minified Bootstrap CSS.pydantic_schemaforms/assets/vendor/bootstrap/bootstrap.bundle.min.jsβ Vendored, minified Bootstrap JS bundle.pydantic_schemaforms/assets/vendor/bootstrap/LICENSEβ Upstream Bootstrap license text.
##### pydantic_schemaforms/assets/vendor/htmx/
pydantic_schemaforms/assets/vendor/htmx/htmx.min.jsβ Vendored, minified HTMX library.pydantic_schemaforms/assets/vendor/htmx/LICENSEβ Upstream HTMX license text.
##### pydantic_schemaforms/assets/vendor/imask/
pydantic_schemaforms/assets/vendor/imask/imask.min.jsβ Vendored, minified IMask library (used for masked inputs).pydantic_schemaforms/assets/vendor/imask/LICENSEβ Upstream IMask license text.
##### pydantic_schemaforms/assets/vendor/materialize/
pydantic_schemaforms/assets/vendor/materialize/materialize.min.cssβ Vendored, minified Materialize CSS.pydantic_schemaforms/assets/vendor/materialize/materialize.min.jsβ Vendored, minified Materialize JS.pydantic_schemaforms/assets/vendor/materialize/LICENSEβ Upstream Materialize license text.
### pydantic_schemaforms/inputs/ (input components)
pydantic_schemaforms/inputs/__init__.pyβ Lazy-loading facade for input classes (keeps import cost low).pydantic_schemaforms/inputs/base.pyβ Base input types, rendering utilities, and shared label/help/error builders.pydantic_schemaforms/inputs/datetime_inputs.pyβ Date/time-related input components.pydantic_schemaforms/inputs/numeric_inputs.pyβ Numeric/slider/range-related input components.pydantic_schemaforms/inputs/registry.pyβ Runtime registry and discovery helpers for input components.pydantic_schemaforms/inputs/selection_inputs.pyβ Select/checkbox/radio/toggle-related input components.pydantic_schemaforms/inputs/specialized_inputs.pyβ Specialized inputs (file upload, color, hidden, csrf/honeypot, tags, etc.).pydantic_schemaforms/inputs/text_inputs.pyβ Text-ish inputs (text, password, email, URL, phone, credit card, etc.).
### pydantic_schemaforms/integration/ (framework/application integration)
pydantic_schemaforms/integration/__init__.pyβ Integration facade with lazy exports of framework glue.pydantic_schemaforms/integration/adapters.pyβ High-level sync/async integration entry points (handle_form,handle_form_async).pydantic_schemaforms/integration/async_support.pyβ Framework-agnostic async request/validation helpers.pydantic_schemaforms/integration/builder.pyβ Form builder utilities (prebuilt forms, page wrapper helpers, asset tag helpers).pydantic_schemaforms/integration/react.pyβ JSON-schema-form-oriented integration helpers.pydantic_schemaforms/integration/schema.pyβ JSON/OpenAPI schema generation utilities.pydantic_schemaforms/integration/sync.pyβ Framework-agnostic sync request/validation helpers.pydantic_schemaforms/integration/utils.pyβ Shared utilities for integrations (type mapping, framework selection, validation conversion).pydantic_schemaforms/integration/vue.pyβ Vue integration helpers.
#### pydantic_schemaforms/integration/frameworks/ (compat + legacy namespace)
pydantic_schemaforms/integration/frameworks/__init__.pyβ Namespace package for framework adapters.pydantic_schemaforms/integration/frameworks/adapters.pyβ Compatibility shim re-exporting the canonical adapter API.pydantic_schemaforms/integration/frameworks/async_support.pyβ Compatibility shim re-exporting async helpers.pydantic_schemaforms/integration/frameworks/sync.pyβ Compatibility shim re-exporting sync helpers.
### pydantic_schemaforms/rendering/ (shared rendering engine)
pydantic_schemaforms/rendering/__init__.pyβ Shared rendering module namespace.pydantic_schemaforms/rendering/context.pyβ Render context objects passed through renderers/layouts.pydantic_schemaforms/rendering/field_renderer.pyβ Field-level rendering logic used by multiple renderers.pydantic_schemaforms/rendering/form_style.pyβFormStylecontract/registry that centralizes framework templates.pydantic_schemaforms/rendering/frameworks.pyβ Framework configuration and input component mapping lookup.pydantic_schemaforms/rendering/layout_engine.pyβ Layout primitives and the engine that renders composed layouts.pydantic_schemaforms/rendering/schema_parser.pyβ Schema parsing/metadata extraction (pydantic model β render plan).pydantic_schemaforms/rendering/theme_assets.pyβ Default CSS/JS snippets for layout-oriented components.pydantic_schemaforms/rendering/themes.pyβ Theme strategies and framework themes (bootstrap/material/plain + embedded variants).
## Beta Release Readiness Assessment
### β Product Vision Alignment
All six Design Rules (Non-Negotiables) are now fully satisfied:
-
Library ships the experience β
- Default output is offline-by-default (vendored HTMX, IMask, Bootstrap, Materialize)
- CDN mode exists but is explicit opt-in and pinned to manifest versions
- All assets are checksummed and verified by tests
-
Pydantic is the single source of truth β
- Validation constraints, required/optional come from Pydantic schema/Field metadata
- UI configuration via
json_schema_extraand form field helpers - Schema generation sanitizes non-serializable objects
-
One obvious way (sync + async) β
- Canonical sync entry point:
handle_form() - Canonical async entry point:
handle_form_async() - Both exported from root package with integration test coverage
- Canonical sync entry point:
-
Renderer outputs deterministic, self-contained HTML β
- No global mutable state in renderer pipeline
- Deterministic output for same model + config
- Theme/style configuration is explicit
-
Debug mode is optional and non-invasive β
- Off by default (
debug=False) - Collapsed panel when enabled, never changes validation/rendering semantics
- Read-only views of: rendered HTML, model source, schema/validation, live payload
- Off by default (
-
Extensibility stays declarative β
- Plugin registration via official registries (
register_input_class,register_layout_renderer) - Extension points compose with themes/styles
- Documented in
docs/plugin_hooks.md
- Plugin registration via official registries (
### β Core Features Complete
Rendering Pipeline: - β Enhanced renderer with schema β fields β layout orchestration - β Theme-driven styling (Bootstrap, Material Design, Plain, Default) - β Field-level rendering with help/error chrome - β Layout engine (Vertical, Horizontal, Tabbed, Accordion) - β Model list support with add/remove functionality - β Async rendering equivalence to sync path
Validation: - β Server-side validation via Pydantic - β HTMX live validation support - β Custom field validators - β Cross-field validation patterns - β Comprehensive validation guide documentation
Integration: - β Flask integration helpers - β FastAPI integration helpers (async-first) - β Framework-agnostic sync/async adapters - β Working examples for both frameworks
Developer Experience: - β Debug mode with live payload inspection - β Comprehensive test suite (250+ tests passing) - β Documentation (quickstart, tutorial, validation guide, assets guide, plugin hooks, testing workflow) - β Ruff linting + pre-commit hooks - β CI/CD pipeline with coverage reporting
### β Quality Metrics
- Test Coverage: 250+ tests passing (see
htmlcov/for detailed report) - Linting: Ruff enabled in pre-commit, zero linting errors
- Python Version: 3.14+ only (clearly documented)
- Dependencies: Minimal (pydantic>=2.7, pydantic-extra-types[all]>=2.10.6)
- Optional Dependencies: FastAPI and Flask marked as optional
### β οΈ Known Limitations (Acceptable for Beta)
-
Dynamic field validation warning: Pydantic emits a
UserWarningwhen usingFormModel.register_field()due tocreate_model()behavior. This is cosmetic and doesn't affect functionality. Can be suppressed or improved in future releases. -
Material Design assets: Currently using Materialize CSS (older). Could be upgraded to Material Web Components or MUI in a future release, but current implementation is functional.
-
Documentation completeness: Core features are documented, but some advanced patterns (custom input components, complex layouts) could benefit from additional examples.
### π Release Checklist
- β All design rules satisfied
- β Core features complete and tested
- β Debug mode implemented
- β Examples working (Flask + FastAPI)
- β Documentation covers essential workflows
- β Test suite passing (250+ tests)
- β Linting clean
- β
Version marked as beta in
pyproject.toml(25.11.3.beta) - β README.md indicates beta status
- β³ CHANGELOG.md updates (should document all changes since last release)
- β³ Release notes prepared (features, breaking changes, migration guide)
### π― Recommendation
pydantic-schemaforms is ready for beta release with the following considerations:
- Update CHANGELOG.md to document all changes, new features, and breaking changes since the last release
- Prepare release notes highlighting:
- Debug mode as a major new feature
- Offline-by-default asset strategy
- Theme-driven rendering system
- Plugin extensibility
- Python 3.14+ requirement (breaking change if upgrading from older versions)
- Tag the release as
v25.11.3-beta(or use current version scheme) - Publish to PyPI with beta classifier
- Announce the beta in relevant communities (Reddit r/Python, Python Discord, etc.) and request feedback
The library has strong architectural foundations, clear design principles, comprehensive test coverage, and working examples. The beta period should focus on: - Gathering user feedback on API ergonomics - Identifying edge cases in real-world usage - Polishing documentation based on user questions - Building community examples/templates
Next Actions After Beta Release: - Monitor issue tracker for bug reports and feature requests - Gather feedback on debug mode usability - Consider Material Web Components migration for v2.0 - Expand documentation with more advanced patterns - Build gallery of community examples
About¶
pydantic-schemaforms is a Python library for generating server-rendered HTML forms from Pydantic models.
It focuses on:
- A simple model β schema β HTML pipeline
- First-class sync (WSGI) and async (ASGI) integration helpers
- Offline-by-default asset delivery (vendored/inlined assets), with explicit opt-in for CDNs
If youβre new, start here:
Project links:
- Docs: https://devsetgo.github.io/pydantic-schemaforms/
- GitHub: https://github.com/devsetgo/pydantic-schemaforms
- PyPI: https://pypi.org/project/pydantic-schemaforms/
About Me¶
I am a software engineering manager with an eclectic background in various industries (finance, manufacturing, and metrology). I am passionate about software development and love to learn new things.