Skip to content

JSON API and JSON Schema

A FormModel is a Pydantic model, so the same class can validate an HTML form and a JSON API request, with the same rules and the same error shape.

One model, two routes

models.py
from pydantic_schemaforms import Field, FormModel


class ContactForm(FormModel):
    name: str = Field(title='Full name', min_length=2, examples=['Ada Lovelace'], ui_autofocus=True)
    email: str = Field(title='Email', examples=['ada@example.com'], ui_element='email')
    message: str = Field(title='Message', min_length=10, ui_element='textarea')
main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from models import ContactForm

from pydantic_schemaforms import html, render_form_html_async

ContactSchema = ContactForm.as_api_model()  # same fields and rules, no ui_* settings
app = FastAPI()


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


@app.post('/contact', response_class=HTMLResponse)
async def submit_contact(request: Request):
    result = ContactForm.validate(await request.form(), submit_url='/contact')
    if not result.is_valid:
        return await result.render_with_errors_async()
    name = result.data['name']
    return html(t'<p>Thanks, {name}!</p>')


@app.post('/api/contact', response_model=ContactSchema)
async def api_contact(contact: ContactSchema):
    return contact

ContactForm.as_api_model() returns a plain Pydantic model with the same fields and the same validation (min_length, title, examples, ...), but without the form settings such as ui_element and ui_autofocus. Use it as the request body or response_model, and FastAPI's OpenAPI docs show a clean schema, exactly as for a model written by hand. Create it once, at import time.

main.py
from flask import Flask, request
from models import ContactForm

from pydantic_schemaforms import html, render_form_html

app = Flask(__name__)


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


@app.post('/contact')
def submit_contact():
    result = ContactForm.validate(request.form, submit_url='/contact')
    if not result.is_valid:
        return result.render_with_errors()
    name = result.data['name']
    return html(t'<p>Thanks, {name}!</p>')


@app.post('/api/contact')
def api_contact():
    result = ContactForm.validate(request.get_json(silent=True) or {})
    if not result.is_valid:
        return {'errors': result.errors}, 422
    return result.data

validate() takes a JSON body as well as a form. Give it the parsed JSON (a nested dict) and it returns the same ValidationResult, so the API route can answer with result.errors, nested like the data, and a 422 status.

JSON Schema for other tools

get_pydantic_json_schema() returns the standard JSON Schema of the form, with nested sub-forms as $defs and $ref, for OpenAPI tooling, schema validators or code generators. Like as_api_model(), it leaves out the form settings. Pydantic's own options pass through:

from pydantic_schemaforms import Field, FormModel


class Address(FormModel):
    street: str = Field(title='Street')
    city: str = Field(title='City')


class PersonForm(FormModel):
    name: str = Field(title='Full name', ui_autofocus=True)
    address: Address = Field(title='Address')


schema = PersonForm.get_pydantic_json_schema()
# {"$defs": {"Address": {...}}, "properties": {"address": {"$ref": "#/$defs/Address"}, ...}}

openapi_schema = PersonForm.get_pydantic_json_schema(ref_template='#/components/schemas/{model}')
get_json_schema()

FormModel.get_json_schema() returns the schema in the shape the form renderer uses internally, with the form settings included and sub-forms not resolved. Use get_pydantic_json_schema() for anything outside the library.