How to Handle and Display Form Validation Errors in Air
Air automatically validates Pydantic-backed forms via AirForm.validate(), stores errors in form.errors, and renders accessible HTML with aria-invalid attributes and user-friendly <small> messages when you call form.render().
The Air framework (feldroy/air) provides a tight integration between Pydantic models and HTML form rendering that makes handling and displaying form validation errors straightforward. By wrapping your data models in AirForm classes, you get automatic error detection, user-friendly message translation, and accessible markup injection without manual template logic.
How AirForm Validates Incoming Data
The AirForm class in src/air/forms.py acts as a thin wrapper around a Pydantic BaseModel. When you call form.validate(data), it runs Pydantic validation and stores the results in two key attributes:
form.errors: A list of PydanticErrorDetailsdictionaries containing raw validation failures.form.is_valid: A boolean indicating whether the form passed validation.
According to the source code at lines 331–335 of src/air/forms.py, the validate method receives raw request data, runs model.validate, and populates these attributes. If validation fails, form.errors contains the specific field-level failures you need to display.
Rendering Accessible Error Messages
Air’s default form renderer, default_form_widget (lines 540–606 in src/air/forms.py), automatically translates raw Pydantic errors into accessible HTML. The renderer performs three critical tasks:
- Maps technical errors to friendly text via
get_user_error_message(lines 396–424), converting types like"int_parsing"into readable phrases such as "Please enter a valid number." - Injects ARIA attributes by adding
aria-invalid="true"to any input field that failed validation, ensuring screen readers announce the error state. - Displays inline messages by inserting a
<small>element (defined insrc/air/tags/models/stock.pyat line 2248) with the ID{field_name}-errorcontaining the translated message.
The HTML generation logic looks like this:
(tags.Small(get_user_error_message(error), id_=f"{field_name}-error") if error else "")
This produces markup like <small id="email-error">This field is required.</small> adjacent to the problematic input.
Complete Implementation Examples
Defining a Basic AirForm
Start by creating a Pydantic model and wrapping it in AirForm:
import air
from pydantic import BaseModel
class ContactModel(BaseModel):
name: str
email: str | None = None
message: str
class ContactForm(air.AirForm):
model = ContactModel
Source: src/air/forms.py (lines 95–106)
Handling POST Requests with Error Display
In your view handler, validate the incoming data and re-render the form if errors exist:
@app.post("/contact")
async def submit(request: air.Request) -> air.Html:
form = ContactForm()
data = await request.form()
form.validate(data) # ← populates form.errors & form.is_valid
if form.is_valid:
return air.Html(air.H1("Thank you!"))
# Errors → render the same form; values are preserved automatically
return air.Html(
air.H1("Please fix the errors below."),
air.Form(
form.render(), # ← includes <small> error messages
air.Button("Send", type_="submit"),
method="post",
action="/contact",
)
)
Key lines: validation at src/air/forms.py lines 331–335; rendering with errors at lines 361–371.
Using Dependency Injection
For FastAPI-compatible dependency injection, use from_request:
from fastapi import Depends
@app.post("/contact")
async def submit(contact: air.AirForm = Depends(ContactForm.from_request)):
if contact.is_valid:
return air.Html(air.H1(f"Hello {contact.data.name}!"))
# Errors are already attached to the form instance
return air.Html(
air.H1("Fix the errors"),
air.Form(contact.render(), air.Button("Resend"), method="post")
)
Source: ContactForm.from_request implementation at src/air/forms.py lines 133–158.
Customizing Error Messages
To override the default user-friendly mappings, monkey-patch get_user_error_message before your forms render:
from air.forms import get_user_error_message as _default_msg
def get_user_error_message(error: dict) -> str:
# Use the built‑in mapping first
msg = _default_msg(error)
# Override a specific case
if error.get("type") == "int_parsing":
return "Numbers only, please."
return msg
Place this customization in a module imported early in your application startup.
Summary
- AirForm wraps Pydantic models to provide
validate()andis_validattributes for error detection according tosrc/air/forms.py(lines 21–82). - Error rendering is automatic via
form.render(), which injectsaria-invalid="true"and<small>elements with user-friendly text viadefault_form_widget(lines 540–606). - Error translation happens through
get_user_error_messageinsrc/air/forms.py(lines 396–424), mapping technical Pydantic error types to readable strings. - Test coverage in
tests/test_forms.py(lines 65–88) confirms that invalid forms produce HTML containing both accessibility attributes and correct error messages. - Customization is possible by overriding the global
get_user_error_messagefunction for project-specific wording.
Frequently Asked Questions
How does Air map Pydantic error types to display messages?
Air uses the get_user_error_message function located at lines 396–424 in src/air/forms.py. This function inspects the type field of each Pydantic ErrorDetails dictionary (e.g., "int_parsing", "missing") and returns a corresponding human-readable string like "Please enter a valid number" or "This field is required."
Can I customize which HTML tag is used for error messages?
The default default_form_widget (lines 540–606) hardcodes the use of the Small tag class from src/air/tags/models/stock.py (line 2248) for inline errors. To use a different tag, you would need to subclass AirForm and override the rendering logic or provide a custom widget function that replaces default_form_widget in your form configuration.
Does Air preserve user input when redisplaying a form with errors?
Yes. When you call form.render() after validation fails, the renderer automatically injects the previously submitted values into each input field’s value attribute. This happens within the default_form_widget logic, ensuring users see their original input alongside the error messages.
How can I test that my form errors render correctly?
The Air test suite demonstrates the expected HTML output in tests/test_forms.py (lines 65–88). You can verify error rendering by creating a form instance, calling validate({}) with empty or invalid data, checking that form.is_valid is False, and asserting that form.render() produces strings containing aria-invalid="true" and your expected error messages within <small> tags.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →