Docs / fastkit-core / Validation

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.

BaseSchema

Pydantic BaseModel with auto-translated error formatting.

Validator Mixins

Password, username, slug — reusable, composable, customizable.

Translated Errors

Error messages in the user's language — automatic from Accept-Language.

Helper Rules

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

python
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:

json
{
  "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.

python
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 typeTranslation key
missingvalidation.required
string_too_shortvalidation.string_too_short
string_too_longvalidation.string_too_long
value_error.emailvalidation.email
greater_than_equalvalidation.greater_than_equal
less_than_equalvalidation.less_than_equal
string_pattern_mismatchvalidation.string_pattern_mismatch

Validation Rules

Helper functions for common field constraints — shorter and more readable than raw Field():

python
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}$')
HelperEquivalent PydanticUse 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.

python
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
8–16 characters
At least one uppercase letter
At least one special character (!@#$%^&*...)

Stronger password validation — all requirements of standard, plus lowercase and digit.

python
from fastkit_core.validation import BaseSchema, StrongPasswordValidatorMixin

class AdminCreate(BaseSchema, StrongPasswordValidatorMixin):
    username: str
    password: str
10–20 characters
At least one uppercase letter
At least one lowercase letter
At least one digit
At least one special character

Username validation — 3–20 characters, alphanumeric and underscore only, cannot start with a number.

python
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
3–20 characters
Letters, numbers and underscore only
Cannot start with a number

Slug validation — lowercase letters, numbers and hyphens only. No consecutive hyphens, no leading/trailing hyphens.

python
from fastkit_core.validation import BaseSchema, SlugValidatorMixin

class PostCreate(BaseSchema, SlugValidatorMixin):
    title: str
    slug:  str    # validated automatically
hello-world
fastkit-core-2025
Hello-World — uppercase
hello--world — consecutive hyphens
-hello — starts with hyphen

Mixins are composable — combine as many as needed on one schema:

python
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

json
{
  "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"
    }
  }
}
json
{
  "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

python
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

python
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)

python
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

python
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

python
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:

python
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:

python
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.

python
@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:

python
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:

python
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

format_errors(exc)
Class method. Converts ValidationError to dict[str, list[str]] with translations applied.
VALIDATION_MESSAGE_MAP
Class variable. Maps Pydantic error types to translation keys. Override to add custom mappings.

Validation Rules

min_length(n)
String field — minimum n characters.
max_length(n)
String field — maximum n characters.
length(min, max)
String field — between min and max characters.
min_value(n)
Numeric field — minimum value n (inclusive).
max_value(n)
Numeric field — maximum value n (inclusive).
between(min, max)
Numeric field — between min and max (inclusive).
pattern(regex)
String field — must match the regular expression.

Validator Mixins

MixinValidates fieldCustomizable via
PasswordValidatorMixinpasswordPWD_MIN_LENGTH, PWD_MAX_LENGTH
StrongPasswordValidatorMixinpasswordPWD_MIN_LENGTH, PWD_MAX_LENGTH
UsernameValidatorMixinusernameUSM_MIN_LENGTH, USM_MAX_LENGTH
SlugValidatorMixinslug

Error Helpers

raise_validation_error(field, message, value)
Raises ValidationError for a single field. value is optional.
raise_multiple_validation_errors(errors)
Raises ValidationError with multiple fields. errors is a list of (field, message, value) tuples.
format_validation_errors(errors)
Converts raw exc.errors() list to dict[str, list[str]]. Nested fields use the last element as key. Unknown fields go under 'unknown'.

Complete Example

python
# 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}
python
# 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"
    )
json
{
  "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"
    }
  }
}