2.4. FastAPI Schema
Schema - also known as Model
Represents data in your system
Pydantic class
schema_extra
is used by Swagger to show examplesEllipsis (
...
) in Pydantic indicates that a Field is required
2.4.1. Example
>>> from pydantic import BaseModel
>>>
>>>
>>> class Astronaut(BaseModel):
... firstname: str
... lastname: str
... active: bool | None = True
2.4.2. Validation
Validators are "class methods", so the first argument value they receive is the Astronaut class, not an instance of Astronaut.
The second argument is always the field value to validate; it can be named as you please
Validators should either return the parsed value or raise a ValueError, TypeError, or AssertionError (assert statements may be used).
Validation is done in the order fields are defined
>>> from string import ascii_uppercase
>>> from pydantic import BaseModel, root_validator, validator
>>>
>>>
>>> class Astronaut(BaseModel):
... firstname: str
... lastname: str
... age: float | None = None
...
... @validator('firstname', 'lastname', allow_reuse=True)
... def is_capitalize(cls, value: str):
... uppercase = tuple(ascii_uppercase)
... if not value.startswith(uppercase):
... raise ValueError('Must starts with uppercase letter')
... return value
...
... @validator('age', allow_reuse=True)
... def age_in_range(cls, value: float):
... if 0 < value < 130:
... return value
... else:
... raise ValueError('Age must be in range from 0 to 130')
...
... @validator('*', allow_reuse=True)
... def not_empty(cls, value: str | float | None):
... if type(value) is str and value == '':
... raise ValueError('Invalid field value')
... return value
...
... @root_validator(pre=True, allow_reuse=True)
... def check_names_differs(cls, values: dict):
... firstname = values.get('firstname')
... lastname = values.get('lastname')
...
... if firstname is None or lastname is None:
... raise ValueError('fields firstname and lastname are not optional')
... if firstname == lastname:
... raise ValueError('firstname and lastname cannot be the same')
... else:
... return values
>>>
>>>
>>> Astronaut(firstname='Mark', lastname='Watney')
Astronaut(firstname='Mark', lastname='Watney', age=None)
>>>
>>> Astronaut(firstname='Mark', lastname='Watney', age=42)
Astronaut(firstname='Mark', lastname='Watney', age=42.0)
>>>
>>> Astronaut(firstname='Mark', lastname='Watney', age=-1)
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 1 validation error for Astronaut
age
Value error, Age must be in range from 0 to 130 [type=value_error, input_value=-1, input_type=int]
For further information visit https://errors.pydantic.dev/.../v/value_error
>>>
>>> Astronaut(firstname='Mark', lastname='Mark', age=1)
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 1 validation error for Astronaut
Value error, firstname and lastname cannot be the same [type=value_error, input_value={'firstname': 'Mark', 'la...name': 'Mark', 'age': 1}, input_type=dict]
For further information visit https://errors.pydantic.dev/.../v/value_error
>>>
>>> Astronaut(firstname='mark', lastname='watney', age=-1)
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 3 validation errors for Astronaut
firstname
Value error, Must starts with uppercase letter [type=value_error, input_value='mark', input_type=str]
For further information visit https://errors.pydantic.dev/.../v/value_error
lastname
Value error, Must starts with uppercase letter [type=value_error, input_value='watney', input_type=str]
For further information visit https://errors.pydantic.dev/.../v/value_error
age
Value error, Age must be in range from 0 to 130 [type=value_error, input_value=-1, input_type=int]
For further information visit https://errors.pydantic.dev/.../v/value_error
>>> from datetime import datetime
>>> from pydantic import BaseModel, validator
>>>
>>>
>>> class PastDate(BaseModel):
... dt: datetime = None
...
... @validator('dt', pre=True, always=True, allow_reuse=True)
... def past_timestamp(cls, value):
... if datetime.fromisoformat(value) >= datetime.now():
... raise ValueError('Timestamp is not in the past')
... return value
>>>
>>> print(PastDate(dt='1969-07-21T02:56:15'))
dt=datetime.datetime(1969, 7, 21, 2, 56, 15)
>>>
>>> print(PastDate(dt='2969-07-21T02:56:15'))
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 1 validation error for PastDate
dt
Value error, Timestamp is not in the past [type=value_error, input_value='2969-07-21T02:56:15', input_type=str]
For further information visit https://errors.pydantic.dev/.../v/value_error
2.4.3. Use Case - 1
schema_extra
is used by Swagger to show examplesEllipsis (
...
) in Pydantic indicates that a Field is required
$ pip install 'pydantic[email]'
>>> from pydantic import BaseModel, EmailStr, Field
>>>
>>>
>>> class StudentSchema(BaseModel):
... fullname: str = Field(...)
... email: EmailStr = Field(...)
... course_of_study: str = Field(...)
... year: int = Field(..., gt=0, lt=9)
... gpa: float = Field(..., le=4.0)
...
... class Config:
... schema_extra = {
... "example": {
... "fullname": "Mark Watney",
... "email": "mark.watney@nasa.gov",
... "course_of_study": "Botanics",
... "year": 2,
... "gpa": "3.0",
... }
... }
>>>
>>>
>>> class UpdateStudentModel(BaseModel):
... fullname: str | None
... email: EmailStr | None
... course_of_study: str | None
... year: int | None
... gpa: float | None
...
... class Config:
... schema_extra = {
... "example": {
... "fullname": "Mark Watney",
... "email": "mark.watney@nasa.gov",
... "course_of_study": "Botanics",
... "year": 4,
... "gpa": "4.0",
... }
... }
2.4.4. Use Case - 1
>>> from fastapi import FastAPI
>>> from fastapi.responses import JSONResponse
>>> from pydantic import BaseModel as Schema
>>> app = FastAPI()
>>>
>>>
>>> SQL_SELECT_LOGIN = """
... SELECT id, firstname, lastname
... FROM users
... WHERE username = :username
... AND password = :password; """
>>>
>>>
>>> class LoginIn(Schema):
... username: str
... password: str
...
... class Config:
... schema_extra = {
... 'example': {
... 'username': 'mwatney',
... 'password': 'nasa'}}
>>>
>>>
>>> class LoginOut(Schema):
... id: int
... firstname: str
... lastname: str
...
... class Config:
... schema_extra = {
... 'example': {
... 'uid': 1,
... 'firstname': 'Mark',
... 'lastname': 'Watney'}}
>>>
>>>
>>> class NotFound(Schema):
... status: int = 404
... reason: str = 'Not Found'
... details: str | None = None
>>>
>>>
>>> @app.post(
... path='/login',
... response_model=LoginOut,
... responses={
... 200: {'model': LoginOut},
... 404: {'model': NotFound}},
... tags=['auth'])
... def login(user: LoginIn):
... from database import DATABASE, SQL_SELECT_LOGIN
... with sqlite3.connect(DATABASE) as db:
... db.row_factory = sqlite3.Row
... if result := db.execute(SQL_SELECT_LOGIN, user.dict()).fetchone():
... return dict(result)
... else:
... return JSONResponse(status_code=404, content={'details': 'invalid login/password'})
2.4.5. Assignments
# FIXME: English translation
# %% About
# - Name: FastAPI Schema Model
# - Difficulty: easy
# - Lines: 15
# - Minutes: 13
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# %% Polish
# 1. Stworzyć dwa endpointy: `GET /astronaut` i `POST /astronaut`
# 2. Endpoint POST, przyjmuje dane w formacie JSON:
# - firstname - wymagane
# - lastname - wymagane
# - age - opcjonalne
# - height - opcjonalne
# - weight - opcjonalne
# - missions - opcjonalna list[str]
# 3. Używając Pydantic Schema (Base Model) zamodeluj dane wejściowe
# 4. Zapisz dane do pliku `FILE`
# 5. Endpoint GET, odczytuje z pliku i wysyła je użytkownikowi
# %% Hints
# - Documentation: http://localhost:8000/docs
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% Imports
import json
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel as Schema
# %% Types
# %% Data
app = FastAPI()
FILE = '_temporary.txt'
# %% Result
if __name__ == '__main__':
uvicorn.run('main:app', host='127.0.0.1', port=8000)