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¶
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')
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.
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.