Skip to content

Fields and Inputs

Each field on the model becomes one input. Most of the time the field's type is enough to pick the right input; Field() adds the label, help text and anything else the form needs.

from datetime import date
from enum import Enum
from typing import Literal

from pydantic_schemaforms import Field, FormModel


class Plan(str, Enum):
    free = 'free'
    team = 'team'
    enterprise = 'enterprise'


class SignupForm(FormModel):
    name: str = Field(title='Full name', min_length=2, ui_autofocus=True)
    email: str = Field(title='Email', ui_element='email', ui_placeholder='you@example.com')
    birthday: date | None = Field(None, title='Birthday')
    plan: Plan = Field(Plan.free, title='Plan', description='You can change plans at any time.')
    team_size: int = Field(1, title='Team size', ge=1, le=500)
    contact: Literal['email', 'phone'] = Field('email', title='Contact me by', ui_element='radio')
    bio: str = Field('', title='About you', ui_element='textarea')
    newsletter: bool = Field(False, title='Send me the newsletter')

Use it with the routes from Handling Submissions; only the model changes. You get a text box for the name, an email box, a date picker, a dropdown of plans, a number box limited to 1–500, two radio buttons, a text area and a checkbox.

The type picks the input

Without a ui_element, the input comes from the field's type:

Field type Input
str Text box
int, float Number box (for a Decimal, set ui_element="decimal" or "currency")
bool Checkbox
Literal[True] Checkbox that must be ticked; see Validation
date Date picker
datetime Date and time picker
time Time picker
EmailStr Email box
AnyUrl URL box
Enum, Literal[...] Dropdown of the values
list[Enum], list[Literal[...]] Multi-select
Another FormModel A sub-form; see Nested Forms and Lists
list of a FormModel A repeating sub-form

plan: Plan above needs nothing else: its dropdown lists free, team and enterprise, and the submitted value comes back as a Plan.

Choosing a different input

Set ui_element when the type alone isn't enough: "email" for a plain str holding an email address, "textarea" for longer text, "radio" to show a few choices side by side instead of a dropdown, "password", "color", "slider", "tags", "file" and many more.

Input Types lists every ui_element with all of its options.

Labels, help text and placeholders

Setting Shows as
title="Full name" The label. Without it, the field name is used: team_size becomes "Team Size".
description="..." Help text under the input
ui_placeholder="you@example.com" Grey hint text inside an empty input
ui_autofocus=True The cursor starts in this field

Help text and error messages are linked to their input for screen readers, and every input gets a label; see Accessibility.

Required and optional fields

A field without a default is required: the form marks its label with * and won't validate without it. Give it a default to make it optional, or None with an Optional type when "no answer" is a valid answer:

birthday: date | None = Field(None, title="Birthday")

An optional dropdown gets an empty first choice, and submitting it empty gives None.

Constraints

Pydantic constraints such as min_length, max_length, ge, le and pattern do two things: they're checked when the form is submitted, and they become HTML attributes (minlength, min, max, pattern) so the browser can catch mistakes before the form is sent. team_size above only accepts 1 to 500 either way. The next chapter, Validation, covers them in detail.

Show a field only when it applies

ui_show_when hides a field until another field has a given value:

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

from pydantic_schemaforms import Field, FormModel, render_form_html_async


class ContactForm(FormModel):
    name: str = Field(title='Full name')
    contact: str = Field(
        'email',
        title='Contact me by',
        ui_element='radio',
        ui_options={
            'choices': [{'value': 'email', 'label': 'Email'}, {'value': 'phone', 'label': 'Phone'}]
        },
    )
    phone: str = Field(
        '',
        title='Phone number',
        ui_show_when={'field': 'contact', 'equals': 'phone', 'required': True},
    )


app = FastAPI()
RENDER_OPTIONS = {'include_framework_assets': True, 'asset_mode': 'cdn'}


@app.get('/contact', response_class=HTMLResponse)
async def show_contact():
    return await render_form_html_async(ContactForm, submit_url='/contact', **RENDER_OPTIONS)


@app.post('/contact', response_class=HTMLResponse)
async def submit_contact(request: Request):
    result = ContactForm.validate(await request.form(), submit_url='/contact', **RENDER_OPTIONS)
    if not result.is_valid:
        return await result.render_with_errors_async()
    return ContactForm.render_display(result.data, **RENDER_OPTIONS)
from flask import Flask, request

from pydantic_schemaforms import Field, FormModel, render_form_html


class ContactForm(FormModel):
    name: str = Field(title='Full name')
    contact: str = Field(
        'email',
        title='Contact me by',
        ui_element='radio',
        ui_options={
            'choices': [{'value': 'email', 'label': 'Email'}, {'value': 'phone', 'label': 'Phone'}]
        },
    )
    phone: str = Field(
        '',
        title='Phone number',
        ui_show_when={'field': 'contact', 'equals': 'phone', 'required': True},
    )


app = Flask(__name__)
RENDER_OPTIONS = {'include_framework_assets': True, 'asset_mode': 'cdn'}


@app.get('/contact')
def show_contact():
    return render_form_html(ContactForm, submit_url='/contact', **RENDER_OPTIONS)


@app.post('/contact')
def submit_contact():
    result = ContactForm.validate(request.form, submit_url='/contact', **RENDER_OPTIONS)
    if not result.is_valid:
        return result.render_with_errors()
    return ContactForm.render_display(result.data, **RENDER_OPTIONS)

field names the field to watch, and one test decides when this one shows:

Test Shows when the other field
"equals": "phone" Has this value
"in": ["phone", "post"] Has one of these values
"truthy": True Is ticked or has a non-empty value (False for the opposite)
"not_empty": True Has any value (False for empty)

The server applies the same rule. A hidden field's submitted value is dropped before validation, even in a hand-written request, so it produces no errors and result.data holds the field's default. That's why the field must have a default.

For "required only when shown", add "required": True to the rule, as above, rather than declaring the field required.

field names the other field as its input is named, so by its alias if it has one. It is relative to the model the rule is on. It can go down into a sub-form ("address.country") but not up to a parent, so to hide a whole sub-form based on a parent's field, put the rule on the sub-form field in the parent. Inside a model_list, each item follows its own values.

In layout="tabbed", "accordion" and "wizard", a hidden sub-form leaves its tab, panel or step in place, empty.

Defaults and pre-filled forms

A fresh form starts from the field defaults: the team size shows 1, the plan is free, the radio button is on "email".

To show existing values, for example to edit a saved record, pass them as form_data:

from models import SignupForm

from pydantic_schemaforms import render_form_html

saved = {'name': 'Ada Lovelace', 'email': 'ada@example.com', 'plan': 'team', 'team_size': 12}

form_html = render_form_html(SignupForm, form_data=saved, submit_url='/signup')

When you pass form_data, it's used as is: fields you leave out are empty, not filled with their defaults. That's what you want when you re-render a submission, where an unticked checkbox is simply missing.

Widget-specific options

Some inputs take extra settings through ui_options, for example the number of rows of a text area or the choices and labels of a select built from a plain str field:

bio: str = Field("", ui_element="textarea", ui_options={"rows": 6})
size: str = Field(
    "m",
    ui_element="select",
    ui_options={"choices": [
        {"value": "s", "label": "Small"},
        {"value": "m", "label": "Medium"},
    ]},
)

Input Types lists every option of every input. Use ui_options only for presentation: a limit such as min/max must be a Pydantic constraint (ge/le) so it's actually enforced.

Next: Validation.