voluptuous validates Python dicts and other data against schemas. pip install voluptuous. Basic: from voluptuous import Schema; s = Schema({"name": str, "age": int}); s({"name": "Alice", "age": 30}). Required: from voluptuous import Required; Schema({Required("name"): str}). Optional: from voluptuous import Optional; Schema({Optional("bio", default=""): str}). Coerce: from voluptuous import Coerce; Schema({"age": Coerce(int)}) — convert str→int. Range: from voluptuous import Range; Schema({"score": Range(min=0, max=100)}). Length: Schema({"name": Length(min=1, max=50)}). All/Any: Schema({"role": Any("admin", "user", "guest")}). Schema({"email": All(str, Email())}). Extra: Schema({...}, extra=ALLOW_EXTRA). extra=REMOVE_EXTRA — strip unknown keys. extra=PREVENT_EXTRA (default). Invalid: try: s(data) except Invalid as e: e.path, e.error_message. MultipleInvalid: except MultipleInvalid as e: e.errors. Humanize: from voluptuous.humanize import humanize_errors; humanize_errors(data, s). Validators: Email(), Url(), FqdnUrl(), Match(r"pattern"). List: Schema([int]) — list of ints. Tuple: Schema((str, int)). Clamp: Clamp(min=0, max=1). In(["a","b"]). Lower(). Upper(). Strip(). Boolean(). Custom: def positive(v): ... raise Invalid("must be > 0"). Claude Code generates voluptuous schema validators, API request validators, and config validation pipelines.
CLAUDE.md for voluptuous
## voluptuous Stack
- Version: voluptuous >= 0.14 | pip install voluptuous
- Schema: Schema({Required("k"): type, Optional("k", default=v): type})
- Coerce: Coerce(int) | Coerce(float) — convert types during validation
- Constraints: Range(min=, max=) | Length(min=, max=) | Any("a","b") | All(str, Length(min=1))
- Extra: extra=ALLOW_EXTRA | REMOVE_EXTRA | PREVENT_EXTRA
- Errors: MultipleInvalid.errors | humanize_errors(data, schema)
voluptuous Validation Pipeline
# app/validation.py — voluptuous schemas, coerce, custom validators, and Flask/FastAPI helpers
from __future__ import annotations
import re
from functools import wraps
from typing import Any, Callable
from voluptuous import (
ALLOW_EXTRA,
PREVENT_EXTRA,
REMOVE_EXTRA,
All,
Any,
Coerce,
In,
Invalid,
Length,
Match,
MultipleInvalid,
Optional,
Range,
Required,
Schema,
Strip,
)
from voluptuous.humanize import humanize_errors
# ─────────────────────────────────────────────────────────────────────────────
# 1. Custom validators
# ─────────────────────────────────────────────────────────────────────────────
def positive_int(v: Any) -> int:
"""Validates and returns a positive integer."""
n = Coerce(int)(v)
if n <= 0:
raise Invalid(f"Expected a positive integer, got {n}")
return n
def non_empty_str(v: Any) -> str:
"""Validates and returns a non-empty stripped string."""
s = str(v).strip()
if not s:
raise Invalid("Value must not be empty")
return s
def email_str(v: Any) -> str:
"""Validates an email-like string (simple pattern)."""
s = str(v).strip().lower()
if not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", s):
raise Invalid(f"Invalid email: {v!r}")
return s
def url_str(v: Any) -> str:
"""Validates an http/https URL."""
s = str(v).strip()
if not re.match(r"^https?://\S+$", s):
raise Invalid(f"Invalid URL: {v!r}")
return s
def slug_str(v: Any) -> str:
"""Validates a URL slug (lowercase alphanumeric + hyphens)."""
s = str(v).strip()
if not re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", s):
raise Invalid(f"Invalid slug: {v!r}")
return s
def one_of(*choices) -> Callable:
"""Returns a validator that accepts only the given choices."""
def _validate(v):
if v not in choices:
raise Invalid(f"Must be one of {choices}, got {v!r}")
return v
return _validate
def list_of(item_validator: Callable) -> Callable:
"""Returns a validator that applies item_validator to every list element."""
def _validate(items):
if not isinstance(items, (list, tuple)):
raise Invalid(f"Expected list, got {type(items).__name__}")
result = []
for i, item in enumerate(items):
try:
result.append(item_validator(item))
except Invalid as e:
raise Invalid(str(e), path=[i]) from e
return result
return _validate
# ─────────────────────────────────────────────────────────────────────────────
# 2. Common schemas
# ─────────────────────────────────────────────────────────────────────────────
USER_SCHEMA = Schema({
Required("name"): All(str, Strip(), Length(min=1, max=100)),
Required("email"): email_str,
Optional("age"): All(Coerce(int), Range(min=0, max=150)),
Optional("role", default="user"): In(["admin", "moderator", "user"]),
Optional("bio", default=""): All(str, Length(max=500)),
}, extra=PREVENT_EXTRA)
PAGINATION_SCHEMA = Schema({
Optional("page", default=1): All(Coerce(int), Range(min=1)),
Optional("per_page", default=20): All(Coerce(int), Range(min=1, max=200)),
Optional("sort_by", default="id"): str,
Optional("order", default="asc"): In(["asc", "desc"]),
}, extra=REMOVE_EXTRA)
ADDRESS_SCHEMA = Schema({
Required("street"): All(str, Strip(), Length(min=1, max=200)),
Required("city"): All(str, Strip(), Length(min=1)),
Optional("state"): All(str, Strip(), Length(min=2, max=50)),
Required("country"): All(str, Strip(), Length(min=2, max=2)), # ISO 3166-1 alpha-2
Optional("zip"): All(str, Strip(), Match(r"^\d{4,10}$")),
})
# ─────────────────────────────────────────────────────────────────────────────
# 3. Validation helpers
# ─────────────────────────────────────────────────────────────────────────────
def validate(schema: Schema, data: Any) -> tuple[Any, list[str]]:
"""
Run schema validation.
Returns (validated_data, []) on success or (None, [error_messages]) on failure.
"""
try:
return schema(data), []
except MultipleInvalid as e:
errors = [f"{'.'.join(str(p) for p in err.path)}: {err.msg}" for err in e.errors]
return None, errors
except Invalid as e:
path = ".".join(str(p) for p in e.path)
return None, [f"{path}: {e.msg}" if path else e.msg]
def validate_or_raise(schema: Schema, data: Any) -> Any:
"""
Validate data against schema; raise ValueError with message on failure.
"""
try:
return schema(data)
except (Invalid, MultipleInvalid) as e:
raise ValueError(str(e)) from e
def validate_many(schema: Schema, items: list[Any]) -> tuple[list[Any], dict[int, list[str]]]:
"""
Validate a list of items.
Returns (valid_items, {index: [errors]}).
"""
valid = []
errors: dict[int, list[str]] = {}
for i, item in enumerate(items):
result, errs = validate(schema, item)
if errs:
errors[i] = errs
else:
valid.append(result)
return valid, errors
def get_human_errors(schema: Schema, data: Any) -> list[str]:
"""
Return human-readable error messages for invalid data.
Returns [] if data is valid.
"""
try:
humanize_errors(data, schema)
return []
except MultipleInvalid as e:
return [str(err) for err in e.errors]
# ─────────────────────────────────────────────────────────────────────────────
# 4. Schema builder
# ─────────────────────────────────────────────────────────────────────────────
class SchemaBuilder:
"""
Fluent schema builder for common field types.
Usage:
s = (SchemaBuilder()
.required("name", str, min_len=1, max_len=100)
.required("email", email_str)
.optional("age", Coerce(int), min_val=0, max_val=150)
.optional("role", default="user")
.build())
"""
def __init__(self, extra=PREVENT_EXTRA):
self._fields: dict = {}
self._extra = extra
def required(
self,
name: str,
typ: Any = str,
min_len: int | None = None,
max_len: int | None = None,
min_val: float | None = None,
max_val: float | None = None,
choices: list | None = None,
) -> "SchemaBuilder":
validators = [typ]
if min_len is not None or max_len is not None:
validators.append(Length(
min=min_len if min_len is not None else 0,
max=max_len,
))
if min_val is not None or max_val is not None:
validators.append(Range(min=min_val, max=max_val))
if choices:
validators.append(In(choices))
self._fields[Required(name)] = All(*validators) if len(validators) > 1 else validators[0]
return self
def optional(
self,
name: str,
typ: Any = str,
default: Any = None,
min_len: int | None = None,
max_len: int | None = None,
choices: list | None = None,
) -> "SchemaBuilder":
validators = [typ]
if min_len is not None or max_len is not None:
validators.append(Length(min=min_len or 0, max=max_len))
if choices:
validators.append(In(choices))
key = Optional(name, default=default)
self._fields[key] = All(*validators) if len(validators) > 1 else validators[0]
return self
def build(self) -> Schema:
return Schema(self._fields, extra=self._extra)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Flask / FastAPI decorator helpers
# ─────────────────────────────────────────────────────────────────────────────
def validated_body(schema: Schema):
"""
FastAPI / Flask decorator — validate request JSON body against schema.
Injects 'validated' key into kwargs on success; returns 422 dict on failure.
Flask usage:
@app.route("/users", methods=["POST"])
@validated_body(USER_SCHEMA)
def create_user(validated=None):
return jsonify(validated)
"""
def decorator(fn: Callable) -> Callable:
@wraps(fn)
def wrapper(*args, **kwargs):
try:
# Flask
from flask import request, jsonify
data = request.get_json(force=True)
except ImportError:
data = kwargs.pop("body", {})
result, errors = validate(schema, data)
if errors:
try:
from flask import jsonify
return jsonify({"errors": errors}), 422
except ImportError:
raise ValueError("; ".join(errors))
kwargs["validated"] = result
return fn(*args, **kwargs)
return wrapper
return decorator
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== USER_SCHEMA — valid ===")
data, errors = validate(USER_SCHEMA, {
"name": " Alice ",
"email": "[email protected]",
"age": "30",
"role": "admin",
})
print("data:", data)
print("errors:", errors)
print("\n=== USER_SCHEMA — invalid ===")
_, errors = validate(USER_SCHEMA, {
"name": "",
"email": "not-an-email",
"age": -5,
"role": "superuser",
"unknown_field": "oops",
})
for e in errors:
print(" ", e)
print("\n=== PAGINATION_SCHEMA — coerce + defaults ===")
page_data, _ = validate(PAGINATION_SCHEMA, {"page": "2", "per_page": "50", "extra": "ignored"})
print("page:", page_data)
print("\n=== Custom validators ===")
slug_schema = Schema({"slug": slug_str, "url": url_str})
ok, _ = validate(slug_schema, {"slug": "hello-world", "url": "https://example.com"})
print("slug+url ok:", ok)
_, errs = validate(slug_schema, {"slug": "Hello World!", "url": "not-a-url"})
print("slug+url errors:", errs)
print("\n=== SchemaBuilder ===")
s = (SchemaBuilder()
.required("title", str, min_len=1, max_len=200)
.required("price", Coerce(float), min_val=0.0)
.optional("category", default="general", choices=["general", "tech", "health"])
.optional("tags", list_of(str), default=[])
.build())
ok, _ = validate(s, {"title": "My Product", "price": "9.99", "category": "tech"})
print("product:", ok)
print("\n=== validate_many ===")
users = [
{"name": "Alice", "email": "[email protected]"},
{"name": "", "email": "bad"},
{"name": "Bob", "email": "[email protected]"},
]
valid_users, errs = validate_many(USER_SCHEMA, users)
print(f"Valid: {len(valid_users)}, Invalid: {len(errs)}")
for idx, msgs in errs.items():
print(f" [item {idx}]:", msgs)
For the marshmallow alternative — marshmallow provides serialization + deserialization + validation with field classes and schema inheritance, suited for ORM integration and REST APIs; voluptuous is simpler and more functional — define schemas as plain Python dicts and functions, no class boilerplate — making it faster to write for config validation and internal data pipelines. For the cerberus alternative — cerberus uses declarative YAML/dict schemas with string-based type names and built-in coercion rules; voluptuous uses Python callables directly as validators, making it easier to compose complex rules with All(), Any(), and lambda functions in Python code. The Claude Skills 360 bundle includes voluptuous skill sets covering Schema with Required/Optional/Coerce, Range/Length/All/Any/In/Match validators, custom validators (email_str, slug_str, list_of), validate()/validate_or_raise()/validate_many() helpers, humanize errors, ALLOW_EXTRA/REMOVE_EXTRA/PREVENT_EXTRA modes, USER_SCHEMA/PAGINATION_SCHEMA/ADDRESS_SCHEMA examples, SchemaBuilder fluent API, and validated_body() Flask/FastAPI decorator. Start with the free tier to try data validation code generation.