Skip to content

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

  1. Keep raw values in form_state / bound state vars while editing.
  2. On submit, call model_validate_json or Pydantic model_validate on a dict you build.
  3. Surface errors with format_validation_errors (streamtree.forms) into Markdown or Text elements.

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:

streamlit run examples/model_form.py

See also