Forms & validation
Goal
Collect user input with StreamTree Form + form_state, and optionally bind a
Pydantic model with streamtree.forms helpers.
Install
Default streamtree includes Pydantic v2. Optional layout helpers live in
streamtree.forms_layout (see Phase 2 form layout).
Pattern A — form_state + widgets
Use when you want commit-on-submit semantics (values update after st.form_submit_button).
Conceptual shape:
from streamtree import component, render
from streamtree.elements import Form, Page, TextInput
from streamtree.state import form_state
@component
def Editor():
title = form_state("", key="title_field")
return Form(
TextInput("Title", value=title),
# ... submit button inside Form in real apps; see examples/model_form.py
form_key="editor",
)
@component
def Root():
return Page(Editor(), key="page")
Pattern B — Pydantic bindings
bind_str_fields, bind_numeric_fields, bind_bool_fields pair model fields
with StateVars so you can render str_text_inputs, number_inputs, etc., from a
schema. See streamtree.forms module docstrings and Phase 2 form layout
for model_field_grid and build_model_from_bindings.
Validation workflow
- Keep raw values in
form_state/ bound state vars while editing. - On submit, call
model_validate_jsonor Pydanticmodel_validateon a dict you build. - Surface errors with
format_validation_errors(streamtree.forms) intoMarkdownorTextelements.
Full runnable reference
"""Pydantic model bound to ``TextInput`` widgets via ``str_text_inputs``."""
from __future__ import annotations
import json
from pydantic import BaseModel, ValidationError
from streamtree import component
from streamtree.app import App
from streamtree.app_context import provider
from streamtree.core.component import render_app
from streamtree.elements import Button, Markdown, Text, ThemeRoot, VStack
from streamtree.forms import (
bind_str_fields,
format_validation_errors,
model_validate_json,
str_text_inputs,
)
from streamtree.state import state
from streamtree.theme import Theme
class Contact(BaseModel):
name: str
email: str
@component
def ContactForm() -> object:
fields = bind_str_fields(Contact, key_prefix="contact_form")
err = state("", key="contact_form_err")
def validate_json() -> None:
try:
payload = {k: fields[k]() for k in fields}
model_validate_json(Contact, json.dumps(payload))
err.set("")
except ValidationError as e:
err.set(format_validation_errors(e))
except (TypeError, ValueError) as e:
err.set(str(e))
return VStack(
ThemeRoot(),
*str_text_inputs(
Contact, bindings=fields, field_labels={"name": "Full name", "email": "Email"}
),
Button("Validate JSON shape", on_click=validate_json),
Markdown(err() or " "),
)
if __name__ == "__main__":
t = Theme(primary_color="#0068c9", custom_css="a { color: var(--st-theme-primary); }\n")
with provider(theme=t):
render_app(
App(
page_title="Model form",
body=VStack(Text("Declarative str fields + Pydantic"), ContactForm()),
)
)
Run:
See also
- Phase 2 form layout — grids, bool fields,
build_model_from_bindings - Phase 3 CRUD — save intent + URL id patterns
- Layouts & error boundaries — wrapping forms in
Card/ErrorBoundary