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.
What render_form_html returns¶
render_form_html() returns an HTML fragment — a <form> element and its supporting scripts, not a complete HTML page. You are responsible for embedding it in your page template and for loading any required CSS framework.
# fragment — embed it inside your own <html>/<head>/<body>
form_html = render_form_html(MyForm, submit_url="/submit")
CSS loading options:
| How you load CSS | Render call |
|---|---|
| Your page already includes Bootstrap/Material | Default — no extra args |
| You want fully self-contained HTML (inlined CSS) | Pass self_contained=True |
You want the renderer to emit <link> tags |
Pass include_framework_assets=True |
If you embed form_html in a plain HTML response without loading Bootstrap/Material, the form will render without styling. Pass self_contained=True for offline/standalone use, or load Bootstrap/Material in your base template.
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, 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.
Future functionality¶
Custom templates¶
A custom template API is planned that will let you replace individual form widgets and layout sections with your own markup, while retaining the library's structural XSS safety guarantees (Python 3.14 t-strings with the html() processor).
This is not yet available for external use. The internal implementation (FormStyleTemplates, TemplateString, FormStyle) is stable, but the public authoring API — how you write and register a custom template — has not been finalised.
Watch the release notes for when this is ready.