Lesson 07: Pydantic Schemas
"Good validation prevents bad data from entering your system."
📚 Table of Contents
- Introduction
- What are Schemas?
- Request Schemas
- Response Schemas
- Validation Rules
- Config Classes
- All Schemas Overview
- Key Takeaways
- Interview Questions
- Exercises
Introduction
Pydantic schemas define the shape of data entering and leaving your API. They provide:
- Automatic validation
- Type safety
- Serialization
- Documentation
In this lesson, you'll understand every schema in schemas.py and why they exist.
What are Schemas?
Models vs Schemas
Models (SQLAlchemy) = Database tables Schemas (Pydantic) = API contracts
# Model - Database
class Agent(Base):
__tablename__ = "agents"
id = Column(Integer)
name = Column(String)
# Schema - API Request
class AgentCreate(BaseModel):
name: str
system_prompt: str
# Schema - API Response
class AgentResponse(BaseModel):
id: int
name: str
created_at: datetimeRequest Schemas
AgentCreate
class AgentCreate(BaseModel):
name: str
type: str
description: str | None = None
system_prompt: str
language: str = "English"
tone: str = "Professional"
enabled_tools: str = ""Purpose: Validate agent creation requests
Usage:
@router.post("/agents/")
def create_agent(agent_data: AgentCreate):
# agent_data is validated automaticallyValidation:
- ✅
nameis required, must be string - ✅
descriptionis optional (can be None) - ✅
languagedefaults to "English"
ChatRequest
class ChatRequest(BaseModel):
message: strSimple but powerful - ensures message exists and is a string.
EmailAnalyzeRequest
class EmailAnalyzeRequest(BaseModel):
email_text: strTaskStatusUpdate
class TaskStatusUpdate(BaseModel):
status: strNote: Validation happens in router (checks allowed values).
DocumentSearchRequest
class DocumentSearchRequest(BaseModel):
query: str
top_k: int = 3top_k: Number of results to return, defaults to 3.
RAGAnswerRequest
class RAGAnswerRequest(BaseModel):
question: str
top_k: int = 3MultiAgentEmailWorkflowRequest
class MultiAgentEmailWorkflowRequest(BaseModel):
email_text: str
create_tasks: bool = Truecreate_tasks: Controls whether tasks are auto-created.
Response Schemas
AgentResponse
class AgentResponse(BaseModel):
id: int
name: str
type: str
description: str | None = None
system_prompt: str
language: str
tone: str
created_at: datetime
enabled_tools: str
class Config:
from_attributes = TruePurpose: Format agent data for API responses
Config.from_attributes = True: Allows creating from SQLAlchemy models
Usage:
@router.get("/agents/{id}", response_model=AgentResponse)
def get_agent(agent_id: int):
agent = db.query(Agent).first()
return agent # Auto-converted to AgentResponseChatResponse
class ChatResponse(BaseModel):
agent_id: int
user_message: str
assistant_response: str
run_id: int
latency_ms: intReturns: Complete chat interaction with metrics.
MessageResponse
class MessageResponse(BaseModel):
id: int
agent_id: int
role: str
content: str
created_at: datetime
class Config:
from_attributes = TrueAgentRunResponse
class AgentRunResponse(BaseModel):
id: int
agent_id: int
input_text: str
output_text: str | None = None
latency_ms: int | None = None
status: str
error_message: str | None = None
created_at: datetimeOptional fields: output_text, latency_ms, error_message can be None.
EmailAnalysisResponse
class EmailAnalysisResponse(BaseModel):
summary: str
intent: str
priority: Literal["low", "medium", "high", "urgent"]
deadline: str | None = None
action_items: list[str]
suggested_reply: str
class Config:
from_attributes = TrueLiteral: Restricts values to specific options.
TaskResponse
class TaskResponse(BaseModel):
id: int
agent_id: int
title: str
description: str | None = None
priority: str
status: str
due_date: str | None = None
source_type: str
created_at: datetime
class Config:
from_attributes = TrueToolCallResponse
class ToolCallResponse(BaseModel):
id: int
agent_id: int
agent_run_id: int | None = None
tool_name: str
tool_input: str | None = None
tool_output: str | None = None
success: str
error_message: str | None = None
latency_ms: int | None = None
created_at: datetime
model_name: str | None = None
estimated_tokens: int | None = None
estimated_cost: str | None = None
class Config:
from_attributes = TrueComplex Nested Schemas
RAGAnswerResponse
class RAGSource(BaseModel):
filename: str
document_id: int
chunk_index: int
score: float | None = None
class RetrievalEvaluation(BaseModel):
has_answer: bool
confidence: float
reason: str
class RAGAnswerResponse(BaseModel):
question: str
answer: str
sources: list[RAGSource]
retrieval_evaluation: RetrievalEvaluation | None = NoneNested structure: Response contains list of sources and evaluation object.
WorkflowReviewResponse
class WorkflowReviewResponse(BaseModel):
approved: bool
quality_score: float
issues: list[str]
recommendation: strMultiAgentEmailWorkflowResponse
class MultiAgentEmailWorkflowResponse(BaseModel):
email_text: str
analysis: dict
tasks_created: list[TaskResponse]
suggested_reply: str
review: WorkflowReviewResponseComplex: Contains analysis dict, list of tasks, and nested review object.
Dashboard Schemas
AgentDashboardResponse
class AgentDashboardResponse(BaseModel):
agent_id: int
agent_name: str
total_tasks: int
open_tasks: int
in_progress_tasks: int
completed_tasks: int
total_tool_calls: int
successful_tool_calls: int
failed_tool_calls: int
total_workflow_runs: int
successful_workflow_runs: int
failed_workflow_runs: int
average_latency_ms: float | None = None
estimated_total_tokens: int
estimated_total_cost: str
average_quality_score: float | None = NoneComprehensive metrics: Everything needed for dashboard visualization.
Validation Rules
Type Validation
class ChatRequest(BaseModel):
message: str # Must be string
# Valid
ChatRequest(message="Hello")
# Invalid - raises ValidationError
ChatRequest(message=123)Optional Fields
class AgentCreate(BaseModel):
name: str
description: str | None = None
# Valid
AgentCreate(name="Agent1")
AgentCreate(name="Agent1", description="Helpful")Default Values
class DocumentSearchRequest(BaseModel):
query: str
top_k: int = 3
# If top_k not provided, uses 3
request = DocumentSearchRequest(query="vacation policy")
# request.top_k == 3Literal Types
from typing import Literal
class EmailAnalysisResponse(BaseModel):
priority: Literal["low", "medium", "high", "urgent"]
# Valid
EmailAnalysisResponse(priority="high")
# Invalid - raises error
EmailAnalysisResponse(priority="extreme")Lists
action_items: list[str]
# Valid
["task 1", "task 2", "task 3"]
# Invalid
["task 1", 123, "task 3"] # 123 is not a stringConfig Classes
from_attributes
class AgentResponse(BaseModel):
id: int
name: str
class Config:
from_attributes = TrueWhat it does: Allows creating from SQLAlchemy models
Without it:
agent = db.query(Agent).first()
return AgentResponse(**agent.__dict__) # Manual conversionWith it:
agent = db.query(Agent).first()
return agent # Automatic conversion!All Schemas Overview
Request Schemas (8)
- AgentCreate
- ChatRequest
- EmailAnalyzeRequest
- TaskStatusUpdate
- DocumentSearchRequest
- RAGAnswerRequest
- MultiAgentEmailWorkflowRequest
Response Schemas (15+)
- AgentResponse
- ChatResponse
- MessageResponse
- AgentRunResponse
- EmailAnalysisResponse
- TaskResponse
- ToolCallResponse
- DocumentResponse
- DocumentChunkResponse
- DocumentSearchResponse
- DocumentSearchResult
- RAGAnswerResponse
- RAGSource
- RetrievalEvaluation
- WorkflowReviewResponse
- MultiAgentEmailWorkflowResponse
- WorkflowStepResponse
- WorkflowRunResponse
- AgentDashboardResponse
- AgentStatsResponse
Key Takeaways
🎯 Schemas validate API data automatically with Pydantic
🎯 Request schemas define what clients must send
🎯 Response schemas define what API returns
🎯 Config.from_attributes enables SQLAlchemy model conversion
🎯 Optional fields use | None = None syntax
🎯 Literal types restrict values to specific options
🎯 Nested schemas enable complex response structures
Interview Questions
Junior
Q1: What's the difference between a model and a schema?
A: Models define database tables (SQLAlchemy). Schemas define API contracts (Pydantic).
Q2: What does str | None = None mean?
A: Field can be a string or None, defaults to None if not provided.
Mid-Level
Q3: Why use Config.from_attributes = True?
A: Allows creating Pydantic models directly from SQLAlchemy models without manual conversion.
Q4: How does Pydantic validation prevent bugs?
A: Rejects invalid data before code executes, preventing type errors and missing fields.
Senior
Q5: Design a schema versioning strategy for API evolution.
A: Use v1/v2 prefixes in schemas, maintain both versions, deprecate old versions gradually with warnings.
Exercises
Exercise 1
Create a UserCreate schema with email, password, and optional profile fields.
Exercise 2
Add validation to ensure top_k is between 1 and 10.
Exercise 3
Create a nested schema for blog posts with author information.
Next Steps
You now understand all schemas in VeyraOps AI.
Next: Services Layer - where business logic lives.
← Previous: Models | ↑ Index | Next: Services →
Module 2 · Data Layer