schema is a minimal Python validation library with a clean API. pip install schema. Basic: from schema import Schema; Schema({"name": str, "age": int}).validate({"name": "Alice", "age": 30}). Optional: from schema import Optional; Schema({Optional("bio"): str}). Or: from schema import Or; Schema({"status": Or("active", "inactive")}). And: from schema import And; Schema({"age": And(int, lambda n: n > 0)}). Use: from schema import Use; Schema({"count": Use(int)}) — coerce. Regex: from schema import Regex; Schema({"email": Regex(r".+@.+")}). Literal: from schema import Literal; Schema({Literal("type", description="must be 'widget'"): "widget"}). Forbidden: from schema import Forbidden; Schema({Forbidden("password"): object}). List: Schema([int]) — validates list of ints. Validate: s.validate(data) → data or raises SchemaError. is_valid: s.is_valid(data) → bool. Error: from schema import SchemaError; try: s.validate(d) except SchemaError as e: e.errors. schema.error — single error message. Nested: Schema({"address": {"city": str, "zip": str}}). JSON: Schema({"items": [{"id": int, "name": str}]}). Custom: Schema(lambda x: x > 0, error="must be positive"). Default: Schema({Optional("theme", default="light"): str}). Claude Code generates schema validation pipelines, config validators, and API request validators.
CLAUDE.md for schema
## schema Stack
- Version: schema >= 0.7 | pip install schema
- Validate: Schema(spec).validate(data) → validated_data | raises SchemaError
- Check: Schema(spec).is_valid(data) → bool (no exception)
- Types: str, int, float, bool — direct type validation
- Coerce: Use(int) | Use(float) — convert types
- Constraints: And(int, lambda n: n > 0) | Or("a","b","c") | Regex(r"pattern")
- Optional: Optional("key", default=val) — missing key uses default
schema Validation Pipeline
# app/schema_utils.py — schema validation, coerce, custom validators, nested schemas
from __future__ import annotations
import re
from typing import Any, Callable
from schema import (
And,
Forbidden,
Literal,
Optional,
Or,
Regex,
Schema,
SchemaError,
Use,
)
# ─────────────────────────────────────────────────────────────────────────────
# 1. Reusable validators
# ─────────────────────────────────────────────────────────────────────────────
# Email — simple RFC-ish pattern
EMAIL_RE = Regex(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", error="Invalid email address")
# URL
URL_RE = Regex(r"^https?://\S+$", error="Invalid URL")
# Slug: lowercase alphanumeric and hyphens
SLUG_RE = Regex(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", error="Invalid slug (use lowercase, numbers, hyphens)")
# ISO 3166-1 alpha-2 country code
COUNTRY_RE = Regex(r"^[A-Z]{2}$", error="Country must be a 2-letter ISO code (e.g. US)")
# Positive number
POS_INT = And(Use(int), lambda n: n > 0, error="Must be a positive integer")
NON_NEG_INT = And(Use(int), lambda n: n >= 0, error="Must be a non-negative integer")
POS_FLOAT = And(Use(float), lambda f: f > 0, error="Must be a positive number")
def in_range(min_val: float, max_val: float) -> And:
"""Validator: number within [min_val, max_val]."""
return And(
Use(float),
lambda n: min_val <= n <= max_val,
error=f"Must be between {min_val} and {max_val}",
)
def min_length(n: int) -> And:
"""Validator: string with at least n chars."""
return And(str, lambda s: len(s.strip()) >= n, error=f"Must be at least {n} chars")
def max_length(n: int) -> And:
"""Validator: string no longer than n chars."""
return And(str, lambda s: len(s) <= n, error=f"Must be at most {n} chars")
def between_length(min_n: int, max_n: int) -> And:
"""Validator: string length within [min_n, max_n]."""
return And(
str,
lambda s: min_n <= len(s.strip()) <= max_n,
error=f"Must be between {min_n} and {max_n} chars",
)
def one_of(*choices) -> Or:
"""Validator: value must be one of the given choices."""
return Or(*choices, error=f"Must be one of {choices}")
def non_empty(v: Any) -> Any:
"""Validator: no None, no empty string, no empty list/dict."""
if v is None or v == "" or v == [] or v == {}:
raise SchemaError("Must not be empty")
return v
# ─────────────────────────────────────────────────────────────────────────────
# 2. Common schemas
# ─────────────────────────────────────────────────────────────────────────────
USER_SCHEMA = Schema({
"name": between_length(1, 100),
"email": And(str, EMAIL_RE),
Optional("age"): And(Use(int), lambda n: 0 <= n <= 150),
Optional("role",
default="user"): one_of("admin", "moderator", "user"),
Optional("website"): And(str, URL_RE),
})
PRODUCT_SCHEMA = Schema({
"id": POS_INT,
"name": between_length(1, 200),
"price": POS_FLOAT,
Optional("sku"): And(str, SLUG_RE),
Optional("stock",
default=0): NON_NEG_INT,
Optional("tags",
default=[]): [str],
Optional("active",
default=True): bool,
})
PAGINATION_SCHEMA = Schema({
Optional("page", default=1): And(Use(int), lambda n: n >= 1),
Optional("per_page", default=20): And(Use(int), lambda n: 1 <= n <= 500),
Optional("sort_by", default="id"): str,
Optional("order", default="asc"): one_of("asc", "desc"),
})
ADDRESS_SCHEMA = Schema({
"street": between_length(1, 200),
"city": min_length(1),
Optional("state"): str,
"country": And(str, COUNTRY_RE),
Optional("postal_code"): And(str, Regex(r"^[\w\s-]{3,10}$")),
})
# ─────────────────────────────────────────────────────────────────────────────
# 3. Validation helpers
# ─────────────────────────────────────────────────────────────────────────────
def validate(schema: Schema, data: Any) -> tuple[Any, str | None]:
"""
Validate data against schema.
Returns (validated, None) on success or (None, error_message) on failure.
"""
try:
return schema.validate(data), None
except SchemaError as e:
return None, str(e)
def is_valid(schema: Schema, data: Any) -> bool:
"""Return True if data passes schema validation."""
return schema.is_valid(data)
def validate_many(
schema: Schema,
items: list[Any],
) -> tuple[list[Any], dict[int, str]]:
"""
Validate a list of items.
Returns (valid_items, {index: error_message}).
"""
valid: list[Any] = []
errors: dict[int, str] = {}
for i, item in enumerate(items):
result, err = validate(schema, item)
if err:
errors[i] = err
else:
valid.append(result)
return valid, errors
def validate_or_raise(schema: Schema, data: Any, error_cls=ValueError) -> Any:
"""Validate; raise error_cls with message on failure."""
result, err = validate(schema, data)
if err:
raise error_cls(err)
return result
def collect_errors(schemas: dict[str, Schema], data: dict[str, Any]) -> dict[str, str]:
"""
Validate each key in data against its named schema.
Returns {field_name: error_message} for invalid fields.
Example:
errors = collect_errors({"email": EMAIL_SCHEMA, "age": AGE_SCHEMA}, form_data)
"""
errors: dict[str, str] = {}
for field, schema in schemas.items():
if field in data:
_, err = validate(schema, data[field])
if err:
errors[field] = err
return errors
# ─────────────────────────────────────────────────────────────────────────────
# 4. Nested schema helpers
# ─────────────────────────────────────────────────────────────────────────────
ORDER_ITEM_SCHEMA = Schema({
"product_id": POS_INT,
"quantity": POS_INT,
"price": POS_FLOAT,
})
ORDER_SCHEMA = Schema({
"order_id": And(str, min_length(1)),
"customer": USER_SCHEMA,
"items": And([ORDER_ITEM_SCHEMA], lambda items: len(items) > 0,
error="Order must have at least one item"),
"address": ADDRESS_SCHEMA,
Optional("notes", default=""): str,
})
# ─────────────────────────────────────────────────────────────────────────────
# 5. Config validation
# ─────────────────────────────────────────────────────────────────────────────
def make_config_schema(fields: dict[str, Any]) -> Schema:
"""
Build a Schema from a {key: validator} dict.
Example:
schema = make_config_schema({
"host": str,
"port": And(Use(int), lambda p: 1 <= p <= 65535),
Optional("debug", default=False): Use(bool),
})
"""
return Schema(fields)
DATABASE_CONFIG_SCHEMA = Schema({
"host": str,
"port": And(Use(int), lambda p: 1 <= p <= 65535, error="Invalid port"),
"name": min_length(1),
"user": str,
Optional("password", default=""): str,
Optional("pool_size", default=5): And(Use(int), lambda n: 1 <= n <= 100),
Optional("ssl", default=False): Use(bool),
})
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== USER_SCHEMA — valid ===")
user, err = validate(USER_SCHEMA, {
"name": "Alice",
"email": "[email protected]",
"age": "30",
"role": "admin",
"website": "https://alice.dev",
})
print("user:", user, "error:", err)
print("\n=== USER_SCHEMA — invalid ===")
_, err = validate(USER_SCHEMA, {
"name": "",
"email": "bad",
"age": -5,
"role": "superuser",
})
print("error:", err[:120] if err else None)
print("\n=== PRODUCT_SCHEMA with coercion ===")
prod, err = validate(PRODUCT_SCHEMA, {
"id": "42",
"name": "Widget",
"price": "9.99",
"tags": ["python", "tool"],
})
print("product:", prod)
print("\n=== validate_many ===")
items = [
{"id": "1", "name": "A", "price": "1.99"},
{"id": "-1", "name": "", "price": "-5"},
{"id": "2", "name": "B", "price": "4.99"},
]
valid_items, errs = validate_many(PRODUCT_SCHEMA, items)
print(f"Valid: {len(valid_items)}, Errors: {dict(errs.items())}")
print("\n=== Nested ORDER_SCHEMA ===")
order = {
"order_id": "ORD-001",
"customer": {"name": "Bob", "email": "[email protected]"},
"items": [
{"product_id": "1", "quantity": "2", "price": "9.99"},
],
"address": {"street": "123 Main St", "city": "Portland", "country": "US"},
}
validated_order, err = validate(ORDER_SCHEMA, order)
print("order valid:", err is None)
if err:
print("error:", err)
print("\n=== DATABASE_CONFIG_SCHEMA ===")
cfg, err = validate(DATABASE_CONFIG_SCHEMA, {
"host": "db.internal",
"port": "5432",
"name": "myapp",
"user": "postgres",
})
print("config:", cfg)
print("\n=== Custom range validator ===")
score_schema = Schema({"score": in_range(0, 100)})
ok, _ = validate(score_schema, {"score": 85})
print("score 85 ok:", ok)
_, err2 = validate(score_schema, {"score": 150})
print("score 150 error:", err2[:80] if err2 else None)
For the voluptuous alternative — voluptuous uses Required/Optional key wrappers, Schema({...}, extra=...) for unknown-key handling, and All()/Any() for compound rules; schema uses a simpler And()/Or() style with direct Optional("key", default=val) and Use(type) coercion — both are functional but schema’s API feels closer to writing natural Python conditions. For the cerberus alternative — cerberus uses string-typed rule dicts ({"type": "integer", "min": 0}) which are easy to serialize but verbose to write; schema defines rules as Python expressions (And(Use(int), lambda n: n >= 0)) that can reference closures, functions, and imported validators directly. The Claude Skills 360 bundle includes schema skill sets covering Schema validate/is_valid, And/Or/Use/Regex/Optional/Forbidden validators, reusable validator constants (EMAIL_RE, SLUG_RE, POS_INT), between_length/in_range/one_of helpers, USER_SCHEMA/PRODUCT_SCHEMA/PAGINATION_SCHEMA/ADDRESS_SCHEMA examples, validate()/validate_many()/collect_errors() helpers, nested ORDER_SCHEMA, DATABASE_CONFIG_SCHEMA, and make_config_schema() factory. Start with the free tier to try schema validation code generation.