Validation
Built on Pydantic 2 — adds automatic translation of error messages, reusable validator mixins, and helper functions for common rules. Your frontend team always gets the same structured error format, in the user's language.
Pydantic BaseModel with auto-translated error formatting.
Password, username, slug — reusable, composable, customizable.
Error messages in the user's language — automatic from Accept-Language.
min_length, max_value, between, pattern — clean field constraints.
Introduction
FastKit Core's validation system wraps Pydantic with three additions that matter in production APIs: consistent error format across every endpoint, translated error messages based on the request locale, and reusable mixins so you don't re-implement password or username rules on every project.
FastKit validation is pure Pydantic underneath — any Pydantic feature works as expected. FastKit only adds on top, never restricts.
Quick Example
from fastkit_core.validation import BaseSchema, min_length, PasswordValidatorMixin
from pydantic import EmailStr
class UserCreate(BaseSchema, PasswordValidatorMixin):
username: str = min_length(3)
email: EmailStr
password: str # validated by PasswordValidatorMixin
age: int | None = None
When validation fails, the error response is always the same structured format — regardless of which field, which rule, or which language:
{
"success": false,
"message": "Validation failed",
"errors": {
"username": ["The username must be at least 3 characters"],
"email": ["The email must be a valid email address"],
"password": [
"Password must be at least 8 characters",
"Password must contain at least one uppercase letter"
]
}
}
BaseSchema
BaseSchema extends Pydantic's BaseModel with one key addition: format_errors() — a class method that converts a ValidationError into the structured {field: [messages]} dict, automatically translated to the current locale.
from fastkit_core.validation import BaseSchema
from pydantic import ValidationError
class ProductCreate(BaseSchema):
name: str
price: float
try:
ProductCreate(name="", price=-10)
except ValidationError as e:
errors = ProductCreate.format_errors(e)
# {
# "name": ["String should have at least 1 character"],
# "price": ["Input should be greater than 0"]
# }
Validation message map
BaseSchema maps Pydantic's internal error types to translation keys, which you define in your translation files:
| Pydantic error type | Translation key |
|---|---|
missing | validation.required |
string_too_short | validation.string_too_short |
string_too_long | validation.string_too_long |
value_error.email | validation.email |
greater_than_equal | validation.greater_than_equal |
less_than_equal | validation.less_than_equal |
string_pattern_mismatch | validation.string_pattern_mismatch |
Validation Rules
Helper functions for common field constraints — shorter and more readable than raw Field():
from fastkit_core.validation import (
BaseSchema,
min_length, max_length, length,
min_value, max_value, between,
pattern
)
class ProductCreate(BaseSchema):
name: str = min_length(3) # at least 3 chars
sku: str = max_length(20) # at most 20 chars
excerpt: str = length(10, 200) # between 10–200 chars
price: float = min_value(0.01) # at least 0.01
stock: int = max_value(10_000) # at most 10,000
rating: float = between(1.0, 5.0) # between 1.0 and 5.0
hex_color: str = pattern(r'^#[0-9A-Fa-f]{6}$')
| Helper | Equivalent Pydantic | Use case |
|---|---|---|
min_length(n) | Field(min_length=n) | Strings with a minimum length |
max_length(n) | Field(max_length=n) | Strings with a maximum length |
length(min, max) | Field(min_length=min, max_length=max) | Strings within a range |
min_value(n) | Field(ge=n) | Numbers with a floor |
max_value(n) | Field(le=n) | Numbers with a ceiling |
between(min, max) | Field(ge=min, le=max) | Numbers within a range |
pattern(regex) | Field(pattern=regex) | Strings matching a regex |
Validator Mixins
Mix in pre-built validators for common patterns. Each mixin validates a specific field by name and is fully customizable via class variables.
Standard password validation — 8–16 characters, at least one uppercase letter and one special character.
from fastkit_core.validation import BaseSchema, PasswordValidatorMixin
class UserCreate(BaseSchema, PasswordValidatorMixin):
username: str
password: str # validated automatically
# Customize defaults
class CustomRegister(BaseSchema, PasswordValidatorMixin):
PWD_MIN_LENGTH = 10 # default: 8
PWD_MAX_LENGTH = 30 # default: 16
password: str
(!@#$%^&*...)Stronger password validation — all requirements of standard, plus lowercase and digit.
from fastkit_core.validation import BaseSchema, StrongPasswordValidatorMixin
class AdminCreate(BaseSchema, StrongPasswordValidatorMixin):
username: str
password: str
Username validation — 3–20 characters, alphanumeric and underscore only, cannot start with a number.
from fastkit_core.validation import BaseSchema, UsernameValidatorMixin
class UserCreate(BaseSchema, UsernameValidatorMixin):
username: str # validated automatically
email: str
# Customize defaults
class CustomUserCreate(BaseSchema, UsernameValidatorMixin):
USM_MIN_LENGTH = 5 # default: 3
USM_MAX_LENGTH = 15 # default: 20
username: str
Slug validation — lowercase letters, numbers and hyphens only. No consecutive hyphens, no leading/trailing hyphens.
from fastkit_core.validation import BaseSchema, SlugValidatorMixin
class PostCreate(BaseSchema, SlugValidatorMixin):
title: str
slug: str # validated automatically
hello-worldfastkit-core-2025Hello-World — uppercasehello--world — consecutive hyphens-hello — starts with hyphenMixins are composable — combine as many as needed on one schema:
from fastkit_core.validation import (
BaseSchema,
UsernameValidatorMixin,
PasswordValidatorMixin,
)
from pydantic import EmailStr
class UserRegistration(
BaseSchema,
UsernameValidatorMixin,
PasswordValidatorMixin
):
username: str # ← validated by UsernameValidatorMixin
password: str # ← validated by PasswordValidatorMixin
email: EmailStr
full_name: str
Translated Error Messages
All validation errors are automatically translated based on the user's locale, set from the Accept-Language header.
Translation file structure
{
"validation": {
"required": "The {field} field is required",
"string_too_short":"The {field} must be at least {min_length} characters",
"string_too_long": "The {field} must not exceed {max_length} characters",
"email": "The {field} must be a valid email address",
"greater_than_equal": "The {field} must be at least {ge}",
"password": {
"min_length": "Password must be at least {min} characters",
"uppercase": "Password must contain at least one uppercase letter",
"special_char":"Password must contain at least one special character"
},
"username": {
"min_length": "Username must be at least {min} characters",
"format": "Username can only contain letters, numbers, and underscores"
},
"slug": {
"format": "Slug must be lowercase letters, numbers, and hyphens only"
}
}
}
{
"validation": {
"required": "El campo {field} es obligatorio",
"string_too_short":"El {field} debe tener al menos {min_length} caracteres",
"string_too_long": "El {field} no debe exceder {max_length} caracteres",
"email": "El {field} debe ser una dirección de correo válida",
"password": {
"min_length": "La contraseña debe tener al menos {min} caracteres",
"uppercase": "La contraseña debe contener al menos una letra mayúscula",
"special_char": "La contraseña debe contener al menos un carácter especial"
}
}
}
Locale middleware
from fastapi import Header, Depends
from fastkit_core.i18n import set_locale
async def detect_language(accept_language: str = Header(default='en')):
locale = accept_language.split(',')[0].split('-')[0]
set_locale(locale)
# Apply to individual routes
@app.post("/users", dependencies=[Depends(detect_language)])
def create_user(user: UserCreate): ...
# Or apply globally via middleware
@app.middleware("http")
async def locale_middleware(request: Request, call_next):
locale = request.headers.get('Accept-Language', 'en')[:2]
set_locale(locale)
return await call_next(request)
Custom Validators
Field validators
from fastkit_core.validation import BaseSchema
from fastkit_core.i18n import _
from pydantic import field_validator
class UserCreate(BaseSchema):
username: str
bio: str
@field_validator('username')
@classmethod
def validate_username(cls, v: str) -> str:
if len(v) < 3:
raise ValueError(_('validation.username.min_length', min=3))
if not v.isalnum():
raise ValueError(_('validation.username.format'))
return v
@field_validator('bio')
@classmethod
def clean_bio(cls, v: str) -> str:
v = ' '.join(v.split()) # collapse whitespace
if len(v) > 500:
raise ValueError(_('validation.string_too_long', field='bio', max_length=500))
return v
Model validators (cross-field)
from pydantic import model_validator
from datetime import date
class EventCreate(BaseSchema):
title: str
start_date: date
end_date: date
@model_validator(mode='after')
def validate_dates(self):
if self.end_date < self.start_date:
raise ValueError(_('validation.end_date_before_start'))
return self
Custom reusable mixin
import re
from typing import ClassVar
from pydantic import field_validator
from fastkit_core.i18n import _
class PhoneValidatorMixin:
PHONE_PATTERN: ClassVar[str] = r'^\+?1?\d{9,15}$'
@field_validator('phone')
@classmethod
def validate_phone(cls, v: str) -> str:
if not re.match(cls.PHONE_PATTERN, v):
raise ValueError(_('validation.phone.format'))
return v
# Usage — just mix in
class UserCreate(BaseSchema, PhoneValidatorMixin):
name: str
phone: str # validated automatically
Error Helpers
Use these when you need to raise validation errors from outside a Pydantic schema — in services, repositories, or exception handlers.
raise_validation_error
from fastkit_core.validation.errors import raise_validation_error
def create_user(email: str):
if user_exists(email):
raise_validation_error('email', 'Email already exists', email)
# → raises ValidationError for the 'email' field
raise_multiple_validation_errors
Collect all errors before raising — so the user sees everything at once, not one error at a time:
from fastkit_core.validation.errors import raise_multiple_validation_errors
def validate_transfer(from_account, to_account, amount):
errors = []
if not account_exists(from_account):
errors.append(('from_account', 'Account not found', from_account))
if not account_exists(to_account):
errors.append(('to_account', 'Account not found', to_account))
if amount <= 0:
errors.append(('amount', 'Amount must be positive', amount))
if errors:
raise_multiple_validation_errors(errors)
# → raises ValidationError with all three fields at once
format_validation_errors
Parse raw Pydantic/FastAPI error lists into clean {field: [messages]} format — same as BaseSchema.format_errors() but for use in exception handlers:
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastkit_core.validation.errors import format_validation_errors
from fastkit_core.http import error_response
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return error_response(
message="Validation failed",
errors=format_validation_errors(exc.errors()),
status_code=422
)
API Integration
FastAPI automatically validates request bodies against your schema. If validation fails, it returns 422 with Pydantic's raw errors. Register a global exception handler to convert those into FastKit's structured format.
@app.post("/products")
def create_product(product: ProductCreate):
# FastAPI validates — 422 on failure, continues on success
return success_response(data=product.model_dump())
For full control over the error response:
from pydantic import ValidationError
from fastkit_core.http import success_response, error_response
@app.post("/products")
def create_product(data: dict):
try:
product = ProductCreate(**data)
return success_response(data=product.model_dump())
except ValidationError as e:
return error_response(
message="Validation failed",
errors=ProductCreate.format_errors(e),
status_code=422
)
Register once for the entire app — all validation errors automatically return FastKit's format:
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastkit_core.validation.errors import format_validation_errors
from fastkit_core.http import error_response
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return error_response(
message="Validation failed",
errors=format_validation_errors(exc.errors()),
status_code=422
)
API Reference
BaseSchema
ValidationError to dict[str, list[str]] with translations applied.Validation Rules
n characters.n characters.min and max characters.n (inclusive).n (inclusive).min and max (inclusive).Validator Mixins
| Mixin | Validates field | Customizable via |
|---|---|---|
PasswordValidatorMixin | password | PWD_MIN_LENGTH, PWD_MAX_LENGTH |
StrongPasswordValidatorMixin | password | PWD_MIN_LENGTH, PWD_MAX_LENGTH |
UsernameValidatorMixin | username | USM_MIN_LENGTH, USM_MAX_LENGTH |
SlugValidatorMixin | slug | — |
Error Helpers
ValidationError for a single field. value is optional.ValidationError with multiple fields. errors is a list of (field, message, value) tuples.exc.errors() list to dict[str, list[str]]. Nested fields use the last element as key. Unknown fields go under 'unknown'.Complete Example
# schemas.py
from fastkit_core.validation import (
BaseSchema,
UsernameValidatorMixin,
PasswordValidatorMixin,
min_length,
)
from pydantic import EmailStr, Field
class UserCreate(BaseSchema, UsernameValidatorMixin, PasswordValidatorMixin):
username: str
email: EmailStr
password: str
full_name: str = min_length(2)
age: int = Field(ge=13, le=120)
class UserUpdate(BaseSchema):
full_name: str | None = None
age: int | None = Field(None, ge=13, le=120)
class UserResponse(BaseSchema):
id: int
username: str
email: str
full_name: str
model_config = {"from_attributes": True}
# main.py
from fastapi import FastAPI, Header, Depends, Request
from fastapi.exceptions import RequestValidationError
from fastkit_core.http import success_response, error_response
from fastkit_core.i18n import set_locale
from fastkit_core.validation.errors import format_validation_errors
from pydantic import ValidationError
from schemas import UserCreate
app = FastAPI()
# Global handler — all 422 errors use FastKit format
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return error_response(
message="Validation failed",
errors=format_validation_errors(exc.errors()),
status_code=422
)
# Locale middleware
@app.middleware("http")
async def locale_middleware(request: Request, call_next):
locale = request.headers.get('Accept-Language', 'en')[:2]
set_locale(locale)
return await call_next(request)
@app.post("/users", status_code=201)
def create_user(user: UserCreate):
# FastAPI validates automatically
# Errors hit the global handler above
return success_response(
data={"username": user.username, "email": user.email},
message="User created successfully"
)
{
"validation": {
"required": "The {field} field is required",
"string_too_short": "The {field} must be at least {min_length} characters",
"string_too_long": "The {field} must not exceed {max_length} characters",
"email": "The {field} must be a valid email address",
"greater_than_equal":"The {field} must be at least {ge}",
"less_than_equal": "The {field} must not exceed {le}",
"password": {
"min_length": "Password must be at least {min} characters",
"max_length": "Password must not exceed {max} characters",
"uppercase": "Password must contain at least one uppercase letter",
"special_char": "Password must contain at least one special character"
},
"username": {
"min_length": "Username must be at least {min} characters",
"max_length": "Username must not exceed {max} characters",
"format": "Username can only contain letters, numbers, and underscores"
},
"slug": {
"format": "Slug must be lowercase letters, numbers, and hyphens only"
}
}
}