Отправка данных в теле запроса, Pydantic модели, валидация
Когда нужно создать или обновить ресурс, данные отправляются в теле запроса. В этой теме вы научитесь использовать Pydantic-модели для валидации, работать с вложенными структурами и обрабатывать разные форматы данных.
GET-запросы используют query-параметры:
GET /items?skip=0&limit=10
Тело запроса пустое, данные в URL.
POST/PUT/PATCH-запросы используют тело запроса:
POST /items
Content-Type: application/json
{
"name": "Laptop",
"price": 999.99,
"in_stock": true
}FastAPI использует Pydantic для описания структуры данных:
from pydantic import BaseModel
class ItemCreate(BaseModel):
name: str
price: float
in_stock: bool = Truename — строка, price — число"999.99" преобразует в floatin_stock будет True, если не передано.model_dump()from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ItemCreate(BaseModel):
name: str
price: float
description: str | None = None
in_stock: bool = True
@app.post('/items')
def create_item(item: ItemCreate):
# item — это экземпляр ItemCreate с валидированными данными
return {
'id': 1,
**item.model_dump()
}ItemCreateitemPOST /items
Content-Type: application/json
{
"name": "Laptop",
"price": 999.99,
"description": "Gaming laptop"
}Ответ:
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"description": "Gaming laptop",
"in_stock": true
}Запрос с неверным типом:
{
"name": "Laptop",
"price": "not_a_number"
}Ответ 422:
{
"detail": [
{
"type": "float_parsing",
"loc": ["body", "price"],
"msg": "Input should be a valid number",
"input": "not_a_number"
}
]
}class ItemCreate(BaseModel):
# Обязательное поле
name: str
# Обязательное поле
price: float
# Необязательное поле (может быть None)
description: str | None = None
# Необязательное поле со значением по умолчанию
in_stock: bool = True
# Необязательное поле со значением по умолчанию
quantity: int = 0| Объявление | Обязательно? | Может быть None? | Значение по умолчанию |
|---|---|---|---|
name: str | Да | Нет | — |
name: str | None | Да | Да | — |
name: str | None = None | Нет | Да | None |
name: str = "default" | Нет | Нет | "default" |
Минимальный (только обязательные):
{"name": "Laptop", "price": 999.99}Полный:
{
"name": "Laptop",
"price": 999.99,
"description": "Gaming",
"in_stock": false,
"quantity": 5
}Модели могут содержать другие модели:
from pydantic import BaseModel
class Address(BaseModel):
city: str
street: str
zip_code: str
class UserCreate(BaseModel):
name: str
email: str
address: AddressПример запроса:
{
"name": "John Doe",
"email": "john@example.com",
"address": {
"city": "Moscow",
"street": "Tverskaya 1",
"zip_code": "123456"
}
}FastAPI рекурсивно валидирует вложенные структуры.
from pydantic import BaseModel
from typing import Literal
class ProductCreate(BaseModel):
name: str
tags: list[str] = [] # Список строк
prices: dict[str, float] = {} # Словарь {регион: цена}
status: Literal['draft', 'published', 'archived'] = 'draft'Пример запроса:
{
"name": "Laptop",
"tags": ["electronics", "computers", "gaming"],
"prices": {
"US": 999.99,
"EU": 899.99,
"RU": 79999.99
},
"status": "published"
}Literal ограничивает набор допустимых значений:
status: Literal['draft', 'published', 'archived']Запрос со status: "invalid" вернёт 422 ошибку.
Для валидации внутри модели используется Field():
from pydantic import BaseModel, Field, EmailStr
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_]+$')
email: EmailStr # Встроенный тип для email
age: int = Field(..., ge=18, le=120)
password: str = Field(..., min_length=8)
bio: str | None = Field(None, max_length=500)| Параметр | Для типа | Описание |
|---|---|---|
ge, le, gt, lt | int, float | Диапазон чисел |
min_length, max_length | str, list | Длина строки/списка |
pattern | str | Regex-шаблон |
... | любой | Обязательное поле |
EmailStr автоматически проверяет формат email:
email: EmailStrЗапрос с "email": "invalid" вернёт:
{
"detail": [
{
"type": "value_error",
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"input": "invalid"
}
]
}from fastapi import FastAPI
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
from typing import Literal
app = FastAPI()
# Модели
class CommentCreate(BaseModel):
content: str = Field(..., min_length=1, max_length=1000)
author: str = Field(..., min_length=3, max_length=50)
class PostCreate(BaseModel):
title: str = Field(..., min_length=5, max_length=200)
content: str = Field(..., min_length=10, max_length=50000)
tags: list[str] = []
status: Literal['draft', 'published', 'scheduled'] = 'draft'
published_at: datetime | None = None
class PostResponse(BaseModel):
id: int
title: str
content: str
tags: list[str]
status: str
created_at: datetime
comments_count: int = 0
# Хранилище (в реальности — база данных)
posts_db = []
comments_db = {}
# Endpoints
@app.post('/posts', response_model=PostResponse, status_code=201)
def create_post(post: PostCreate):
post_id = len(posts_db) + 1
new_post = {
'id': post_id,
**post.model_dump(),
'created_at': datetime.now(),
'comments_count': 0
}
posts_db.append(new_post)
comments_db[post_id] = []
return new_post
@app.post('/posts/{post_id}/comments')
def add_comment(post_id: int, comment: CommentCreate):
if post_id not in comments_db:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Post not found")
comment_data = {
'id': len(comments_db[post_id]) + 1,
**comment.model_dump(),
'created_at': datetime.now()
}
comments_db[post_id].append(comment_data)
# Увеличиваем счётчик комментариев
for post in posts_db:
if post['id'] == post_id:
post['comments_count'] += 1
break
return comment_data
@app.get('/posts/{post_id}', response_model=PostResponse)
def get_post(post_id: int):
for post in posts_db:
if post['id'] == post_id:
return post
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Post not found")Хорошая практика — разные модели для создания, обновления и ответа:
class UserCreate(BaseModel):
"""Модель для создания пользователя"""
username: str = Field(..., min_length=3)
email: EmailStr
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
"""Модель для обновления (все поля необязательны)"""
username: str | None = Field(None, min_length=3)
email: EmailStr | None = None
password: str | None = Field(None, min_length=8)
class UserResponse(BaseModel):
"""Модель для ответа (без пароля)"""
id: int
username: str
email: str
created_at: datetime
class Config:
from_attributes = True # Для работы с ORMUserCreate:
UserUpdate:
UserResponse:
id, created_at (генерируются сервером)@app.get('/items')
def create_item(item: ItemCreate): # Ошибка: GET не должен иметь body
...Проблема: GET-запросы не должны иметь тело. Данные передаются через query-параметры.
Решение: Используйте POST для отправки body:
@app.post('/items')
def create_item(item: ItemCreate):
...@app.post('/users', response_model=UserCreate)
def create_user(user: UserCreate):
return {'id': 1, **user.model_dump()} # Вернёт пароль!Проблема: В ответе будет пароль пользователя.
Решение: Используйте response model без пароля:
@app.post('/users', response_model=UserResponse)
def create_user(user: UserCreate):
return {'id': 1, 'username': user.username, 'email': user.email, 'created_at': datetime.now()}class ItemCreate(BaseModel):
name: str
price: float
@app.post('/items')
def create_item(item: ItemCreate):
item.id = 1 # Ошибка: Pydantic модели immutable по умолчанию
return itemРешение: Создайте новую модель или используйте словарь:
@app.post('/items')
def create_item(item: ItemCreate):
return {'id': 1, **item.model_dump()}Клиент отправляет без заголовка:
POST /items
{"name": "Laptop"}Проблема: Некоторые клиенты не устанавливают Content-Type: application/json.
Решение: FastAPI автоматически определит JSON, но лучше требовать правильный заголовок. В документации Swagger UI заголовок устанавливается автоматически.
Откройте /docs, найдите POST-эндпоинт:
import logging
import json
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
@app.post('/items')
def create_item(item: ItemCreate):
logger.debug(f"Received item: {json.dumps(item.model_dump(), indent=2)}")
return {'id': 1, **item.model_dump()}В следующей теме вы изучите response model — как форматировать ответы, скрывать поля и настраивать статус-коды.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.