Install
$ agentstack add skill-jartan-llc-grimoire-pydantic ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Pydantic Validation Skill
Quick Start
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
class User(BaseModel):
id: int
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
created_at: datetime = Field(default_factory=datetime.now)
is_active: bool = True
# Validate data
user = User(id=1, name="Alice", email="alice@example.com")
print(user.model_dump()) # {'id': 1, 'name': 'Alice', ...}
# Automatic type coercion
user2 = User(id="2", name="Bob", email="bob@example.com")
assert user2.id == 2 # String "2" coerced to int
# Validation error
try:
User(id=3, name="", email="invalid")
except ValidationError as e:
print(e.errors())
Core Concepts
BaseModel Foundation
from pydantic import BaseModel, ConfigDict
class Product(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
use_enum_values=True,
arbitrary_types_allowed=False
)
name: str
price: float
quantity: int = 0
# Usage
product = Product(name=" Widget ", price=19.99)
assert product.name == "Widget" # Whitespace stripped
# Validate on assignment
product.price = "29.99" # Auto-converts to float
Field Configuration
from pydantic import Field, field_validator
from typing import Annotated
class Item(BaseModel):
# Field constraints
sku: str = Field(pattern=r'^[A-Z]{3}-\d{4}$')
price: float = Field(gt=0, le=10000)
stock: int = Field(ge=0, default=0)
# Annotated types (Pydantic v2)
quantity: Annotated[int, Field(ge=1, le=100)]
# Descriptions and examples
description: str = Field(
...,
description="Product description",
examples=["High-quality widget"]
)
# Deprecated fields
old_field: str | None = Field(None, deprecated=True)
@field_validator('sku')
@classmethod
def validate_sku(cls, v: str) -> str:
if not v.startswith('ABC'):
raise ValueError('SKU must start with ABC')
return v
Pydantic v2 Improvements
Migration from v1
# Pydantic v1
class OldModel(BaseModel):
class Config:
validate_assignment = True
json_encoders = {datetime: lambda v: v.isoformat()}
# Pydantic v2
class NewModel(BaseModel):
model_config = ConfigDict(
validate_assignment=True,
# json_encoders replaced by serializers
)
@model_serializer
def ser_model(self) -> dict:
return {...}
# Key changes:
# - .dict() -> .model_dump()
# - .json() -> .model_dump_json()
# - .parse_obj() -> .model_validate()
# - .parse_raw() -> .model_validate_json()
# - @validator -> @field_validator
# - @root_validator -> @model_validator
Performance Improvements
# v2 uses Rust core (pydantic-core) for 5-50x speedup
from pydantic import BaseModel
import time
class Data(BaseModel):
values: list[int]
names: list[str]
# Benchmark
data = {'values': list(range(10000)), 'names': ['item'] * 10000}
start = time.perf_counter()
for _ in range(1000):
Data.model_validate(data)
elapsed = time.perf_counter() - start
print(f"Validated 1000 iterations in {elapsed:.2f}s")
Field Types
Built-in Types
from pydantic import (
BaseModel, EmailStr, HttpUrl, UUID4,
FilePath, DirectoryPath, Json, SecretStr,
PositiveInt, NegativeFloat, conint, constr
)
from typing import Literal
from pathlib import Path
class Example(BaseModel):
# Email validation
email: EmailStr
# URL validation
website: HttpUrl
# UUID
id: UUID4
# File system paths
config_file: FilePath
data_dir: DirectoryPath
# JSON string -> parsed object
metadata: Json[dict[str, str]]
# Secret (won't print in logs)
api_key: SecretStr
# Constrained types
age: PositiveInt
balance: NegativeFloat
username: constr(min_length=3, max_length=20, pattern=r'^[a-z]+$')
code: conint(ge=1000, le=9999)
# Literal types
status: Literal['pending', 'approved', 'rejected']
Custom Types
from pydantic import GetCoreSchemaHandler
from pydantic_core import core_schema
from typing import Any
class Color:
def __init__(self, r: int, g: int, b: int):
self.r, self.g, self.b = r, g, b
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.no_info_after_validator_function(
cls.validate,
core_schema.str_schema()
)
@classmethod
def validate(cls, v: str) -> 'Color':
if not v.startswith('#') or len(v) != 7:
raise ValueError('Invalid hex color')
r = int(v[1:3], 16)
g = int(v[3:5], 16)
b = int(v[5:7], 16)
return cls(r, g, b)
class Design(BaseModel):
primary_color: Color
# Usage
design = Design(primary_color='#FF5733')
assert design.primary_color.r == 255
Validators
Field Validators
from pydantic import field_validator, model_validator
class Account(BaseModel):
username: str
password: str
password_confirm: str
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError('must be alphanumeric')
return v
@field_validator('password')
@classmethod
def password_strong(cls, v: str) -> str:
if len(v) str:
if not v or not v.strip():
raise ValueError('must not be empty')
return v.strip()
Model Validators
from pydantic import model_validator
from typing import Self
class DateRange(BaseModel):
start_date: datetime
end_date: datetime
@model_validator(mode='after')
def check_dates(self) -> Self:
if self.end_date dict:
# Pre-processing before validation
if isinstance(data, dict) and 'total' not in data:
data['total'] = len(data.get('items', [])) * 10.0
return data
Root Validators (Wrap)
from pydantic import model_validator, ValidationInfo
class Config(BaseModel):
env: Literal['dev', 'prod']
debug: bool = False
@model_validator(mode='wrap')
@classmethod
def validate_config(cls, values: Any, handler, info: ValidationInfo):
# Call default validation
result = handler(values)
# Post-validation logic
if result.env == 'prod' and result.debug:
raise ValueError('debug cannot be True in production')
return result
Type Coercion and Strict Mode
from pydantic import BaseModel, ConfigDict, ValidationError
# Coercive mode (default)
class CoerciveModel(BaseModel):
count: int
price: float
data = CoerciveModel(count="42", price="19.99")
assert data.count == 42 # String -> int
assert data.price == 19.99 # String -> float
# Strict mode
class StrictModel(BaseModel):
model_config = ConfigDict(strict=True)
count: int
price: float
try:
StrictModel(count="42", price="19.99") # Raises ValidationError
except ValidationError as e:
print("Strict mode: no coercion allowed")
# Per-field strict mode
class MixedModel(BaseModel):
flexible: int # Allows coercion
strict: Annotated[int, Field(strict=True)] # No coercion
MixedModel(flexible="1", strict=2) # OK
# MixedModel(flexible="1", strict="2") # ValidationError
Nested Models and Recursive Types
from pydantic import BaseModel
from typing import ForwardRef
# Nested models
class Address(BaseModel):
street: str
city: str
country: str
class Company(BaseModel):
name: str
address: Address
company = Company(
name="ACME Corp",
address={'street': '123 Main St', 'city': 'NYC', 'country': 'USA'}
)
# Recursive types (tree structure)
class TreeNode(BaseModel):
value: int
children: list['TreeNode'] = []
TreeNode.model_rebuild() # Required for forward references
tree = TreeNode(
value=1,
children=[
TreeNode(value=2, children=[TreeNode(value=4)]),
TreeNode(value=3)
]
)
# Self-referencing with ForwardRef
class Category(BaseModel):
name: str
parent: 'Category | None' = None
subcategories: list['Category'] = []
Category.model_rebuild()
Generic Models
from pydantic import BaseModel
from typing import Generic, TypeVar
T = TypeVar('T')
class Response(BaseModel, Generic[T]):
success: bool
data: T
message: str = ''
class User(BaseModel):
id: int
name: str
# Usage with concrete type
user_response = Response[User](
success=True,
data=User(id=1, name='Alice')
)
# List response
list_response = Response[list[User]](
success=True,
data=[User(id=1, name='Alice'), User(id=2, name='Bob')]
)
# Generic repository pattern
class Repository(BaseModel, Generic[T]):
items: list[T]
def add(self, item: T) -> None:
self.items.append(item)
user_repo = Repository[User](items=[])
user_repo.add(User(id=1, name='Alice'))
Serialization
Model Dump
from pydantic import BaseModel, Field, field_serializer
class Article(BaseModel):
title: str
content: str
tags: list[str]
metadata: dict[str, Any] = {}
# Serialization customization
@field_serializer('tags')
def serialize_tags(self, tags: list[str]) -> str:
return ','.join(tags)
article = Article(
title='Pydantic Guide',
content='...',
tags=['python', 'validation']
)
# Dump to dict
data = article.model_dump()
# {'title': 'Pydantic Guide', 'tags': 'python,validation', ...}
# Exclude fields
data = article.model_dump(exclude={'metadata'})
# Include only specific fields
data = article.model_dump(include={'title', 'tags'})
# Exclude unset fields
article2 = Article(title='Test', content='...', tags=[])
data = article2.model_dump(exclude_unset=True) # metadata excluded
# By alias
class AliasModel(BaseModel):
internal_name: str = Field(alias='externalName')
model = AliasModel(externalName='value')
model.model_dump(by_alias=True) # {'externalName': 'value'}
JSON Serialization
from datetime import datetime
from pydantic import BaseModel, field_serializer
class Event(BaseModel):
name: str
timestamp: datetime
@field_serializer('timestamp')
def serialize_dt(self, dt: datetime) -> str:
return dt.isoformat()
event = Event(name='Deploy', timestamp=datetime.now())
# Dump to JSON string
json_str = event.model_dump_json()
# '{"name":"Deploy","timestamp":"2025-11-30T..."}'
# Pretty print
json_str = event.model_dump_json(indent=2)
# Parse from JSON
event2 = Event.model_validate_json(json_str)
Custom Serializers
from pydantic import model_serializer
class User(BaseModel):
id: int
username: str
password: SecretStr
@model_serializer
def ser_model(self) -> dict[str, Any]:
return {
'id': self.id,
'username': self.username,
# Never serialize password
}
user = User(id=1, username='alice', password='secret123')
assert 'password' not in user.model_dump()
Settings Management
BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(
env_file='.env',
env_file_encoding='utf-8',
env_prefix='APP_',
case_sensitive=False
)
# Environment variables
database_url: str
redis_url: str = 'redis://localhost:6379'
secret_key: SecretStr
debug: bool = False
# Nested settings
class SMTPSettings(BaseModel):
host: str
port: int = 587
username: str
password: SecretStr
smtp: SMTPSettings
# Reads from environment variables:
# APP_DATABASE_URL, APP_REDIS_URL, APP_SECRET_KEY, APP_DEBUG
# APP_SMTP__HOST, APP_SMTP__PORT, etc.
settings = AppSettings()
Multi-Environment Settings
from functools import lru_cache
class Settings(BaseSettings):
environment: Literal['dev', 'staging', 'prod'] = 'dev'
database_url: str
api_key: SecretStr
model_config = SettingsConfigDict(
env_file='.env',
extra='ignore'
)
@property
def is_production(self) -> bool:
return self.environment == 'prod'
@lru_cache
def get_settings() -> Settings:
return Settings()
# Usage in FastAPI
from fastapi import Depends
@app.get('/config')
def get_config(settings: Settings = Depends(get_settings)):
return {'env': settings.environment}
FastAPI Integration
Request/Response Models
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=50)
email: EmailStr
password: str = Field(min_length=8)
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
model_config = ConfigDict(from_attributes=True)
@app.post('/users', response_model=UserResponse)
def create_user(user: UserCreate):
# FastAPI auto-validates request body
# Returns only fields in UserResponse (password excluded)
return UserResponse(
id=1,
username=user.username,
email=user.email
)
Query Parameters
from pydantic import BaseModel, Field
from fastapi import Query
class PaginationParams(BaseModel):
skip: int = Field(0, ge=0)
limit: int = Field(10, ge=1, le=100)
class SearchParams(BaseModel):
q: str = Field(..., min_length=1)
category: str | None = None
sort_by: Literal['date', 'relevance'] = 'relevance'
@app.get('/search')
def search(params: SearchParams = Query()):
return {'query': params.q, 'sort': params.sort_by}
Response Model Customization
class DetailedUser(BaseModel):
id: int
username: str
email: EmailStr
created_at: datetime
last_login: datetime | None
@app.get('/users/{user_id}', response_model=DetailedUser)
def get_user(user_id: int, include_dates: bool = False):
user = DetailedUser(
id=user_id,
username='alice',
email='alice@example.com',
created_at=datetime.now(),
last_login=None
)
if not include_dates:
return user.model_dump(exclude={'created_at', 'last_login'})
return user
SQLAlchemy Integration
ORM Models with Pydantic
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import DeclarativeBase
from pydantic import BaseModel, ConfigDict
class Base(DeclarativeBase):
pass
# SQLAlchemy ORM model
class UserDB(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True)
email = Column(String(100))
created_at = Column(DateTime, default=datetime.utcnow)
# Pydantic model for validation
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
email: EmailStr
created_at: datetime
# Usage
from sqlalchemy.orm import Session
def get_user(db: Session, user_id: int) -> UserSchema:
user = db.query(UserDB).filter(UserDB.id == user_id).first()
return UserSchema.model_validate(user) # ORM -> Pydantic
Hybrid Approach
from pydantic import BaseModel
class UserBase(BaseModel):
username: str
email: EmailStr
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
username: str | None = None
email: EmailStr | None = None
password: str | None = None
class UserInDB(UserBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
password_hash: str
# CRUD operations
def create_user(db: Session, user: UserCreate) -> Use
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [Jartan-LLC](https://github.com/Jartan-LLC)
- **Source:** [Jartan-LLC/grimoire](https://github.com/Jartan-LLC/grimoire)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.