schematics provides class-based data models with validation and serialization. pip install schematics. Model: from schematics.models import Model; from schematics.types import StringType, IntType. Define: class User(Model): name = StringType(required=True); age = IntType(min_value=0). Create: u = User({"name": "Alice", "age": 30}). Validate: u.validate() — raises DataError. from schematics.exceptions import DataError. u.errors — dict of errors. to_primitive: u.to_primitive() → dict. to_native: User(data).to_native(). Serialize: u.serialize() — same as to_primitive. Roles: class User(Model): class Options: roles = {"public": blacklist("password")}; u.to_primitive(role="public"). Types: StringType, IntType, FloatType, BooleanType, LongType, DateTimeType, DateType, UUIDType, EmailType, URLType, MD5Type, IPv4Type. ListType(StringType()). DictType(IntType()). ModelType(Address) — nested model. PolyModelType({"user":User,"org":Org}). Default: StringType(default="N/A"). Choices: StringType(choices=["a","b"]). Regex: from schematics.types import StringType; StringType(regex=r"\d+"). Custom: from schematics.types.base import BaseType; class PositiveIntType(BaseType): def validate_positive(self, value, data): if value <= 0: raise ConversionError("Must be positive"). Serializer keys: serialized_name="camelCase". Import: ModelType, ListType, PolyModelType. Claude Code generates schematics data models, API request/response models, and validation pipelines.
CLAUDE.md for schematics
## schematics Stack
- Version: schematics >= 2.1 | pip install schematics
- Model: class MyModel(Model): field = TypeClass(required=True, ...)
- Validate: model.validate() → raises DataError | model.errors → dict
- Serialize: model.to_primitive() → dict | model.serialize(role="public")
- Roles: class Options: roles = {"public": blacklist("secret_field")}
- Nested: ModelType(OtherModel) | ListType(ModelType(Item)) | PolyModelType(map)
- Custom: subclass BaseType, add validate_<rule>(self, value, data) methods
schematics Data Modeling Pipeline
# app/models.py — schematics Model, types, validation, roles, and nested models
from __future__ import annotations
import uuid
from typing import Any
from schematics.exceptions import ConversionError, DataError, ValidationError
from schematics.models import Model
from schematics.transforms import blacklist, whitelist
from schematics.types import (
BooleanType,
DateTimeType,
EmailType,
FloatType,
IntType,
ListType,
StringType,
UUIDType,
URLType,
)
from schematics.types.compound import DictType, ModelType
from schematics.types.base import BaseType
# ─────────────────────────────────────────────────────────────────────────────
# 1. Custom field types
# ─────────────────────────────────────────────────────────────────────────────
class SlugType(StringType):
"""
StringType that validates slug format: lowercase alphanumeric + hyphens.
Usage: slug = SlugType(required=True)
"""
MESSAGES = {"invalid_slug": "Must be a valid slug (lowercase letters, digits, hyphens)"}
def validate_slug(self, value, data):
import re
if not re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", value):
raise ValidationError(self.MESSAGES["invalid_slug"])
class PositiveIntType(IntType):
"""IntType that enforces value > 0."""
MESSAGES = {"positive": "Must be a positive integer (> 0)"}
def validate_positive(self, value, data):
if value <= 0:
raise ValidationError(self.MESSAGES["positive"])
class NonNegativeFloatType(FloatType):
"""FloatType that enforces value >= 0."""
def validate_non_negative(self, value, data):
if value < 0:
raise ValidationError("Must be a non-negative number")
# ─────────────────────────────────────────────────────────────────────────────
# 2. Base models
# ─────────────────────────────────────────────────────────────────────────────
class TimestampedMixin(Model):
"""
Mixin that adds created_at and updated_at fields.
Not a standalone model — inherit from this alongside your model base.
"""
created_at = DateTimeType()
updated_at = DateTimeType()
class IdentifiedMixin(Model):
"""Mixin that adds an id field (UUID string)."""
id = UUIDType(default=uuid.uuid4)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Domain models
# ─────────────────────────────────────────────────────────────────────────────
class Address(Model):
"""Embeddable address model."""
street = StringType(required=True, min_length=1, max_length=200)
city = StringType(required=True, min_length=1)
state = StringType(max_length=50)
country = StringType(required=True, min_length=2, max_length=2)
postal_code = StringType(max_length=20)
class UserProfile(Model):
"""Nested profile embedded in User."""
bio = StringType(max_length=500)
website = URLType()
location = StringType()
class User(Model):
"""
User model with roles for public and admin serialization.
"""
id = UUIDType(default=uuid.uuid4)
name = StringType(required=True, min_length=1, max_length=100)
email = EmailType(required=True)
role = StringType(choices=["admin", "moderator", "user"], default="user")
active = BooleanType(default=True)
password = StringType() # excluded from "public" role
profile = ModelType(UserProfile)
address = ModelType(Address)
tags = ListType(StringType(), default=list)
class Options:
roles = {
"public": blacklist("password"),
"admin": whitelist("id", "name", "email", "role", "active"),
"minimal": whitelist("id", "name"),
}
def validate_name(self, data, value):
"""Cross-field validator: name must be non-whitespace."""
if not value.strip():
raise ValidationError("Name must not be blank")
return value.strip()
class Product(Model):
"""Product model with pricing and inventory."""
id = PositiveIntType(required=True)
name = StringType(required=True, min_length=1, max_length=200)
slug = SlugType(required=True)
price = NonNegativeFloatType(required=True)
stock = IntType(default=0, min_value=0)
active = BooleanType(default=True)
tags = ListType(StringType(), default=list)
metadata = DictType(StringType())
class Options:
roles = {
"public": blacklist("metadata"),
"summary": whitelist("id", "name", "price", "slug"),
}
class OrderItem(Model):
"""Line item in an order."""
product_id = PositiveIntType(required=True)
quantity = PositiveIntType(required=True)
unit_price = NonNegativeFloatType(required=True)
@property
def line_total(self) -> float:
if self.quantity and self.unit_price:
return round(self.quantity * self.unit_price, 2)
return 0.0
class Order(Model):
"""Order with nested items and customer reference."""
order_id = StringType(required=True, min_length=1)
customer = ModelType(User, required=True)
items = ListType(ModelType(OrderItem), required=True, min_size=1)
shipping = ModelType(Address)
notes = StringType(default="")
status = StringType(choices=["pending","confirmed","shipped","delivered","cancelled"],
default="pending")
def validate_items(self, data, value):
if not value:
raise ValidationError("Order must have at least one item")
return value
# ─────────────────────────────────────────────────────────────────────────────
# 4. Validation helpers
# ─────────────────────────────────────────────────────────────────────────────
def validate_model(model: Model) -> tuple[bool, dict[str, Any]]:
"""
Validate a schematics model instance.
Returns (True, {}) on success or (False, {field: [errors]}) on failure.
"""
try:
model.validate()
return True, {}
except DataError as e:
return False, _flatten_errors(e.errors)
def _flatten_errors(errors: Any, prefix: str = "") -> dict[str, list[str]]:
"""Recursively flatten DataError error dict to {field_path: [messages]}."""
flat: dict[str, list[str]] = {}
if isinstance(errors, dict):
for key, val in errors.items():
full_key = f"{prefix}.{key}" if prefix else str(key)
flat.update(_flatten_errors(val, full_key))
elif isinstance(errors, list):
messages = []
for item in errors:
if isinstance(item, Exception):
messages.append(str(item))
elif isinstance(item, dict):
flat.update(_flatten_errors(item, prefix))
else:
messages.append(str(item))
if messages:
flat[prefix] = messages
elif isinstance(errors, Exception):
flat[prefix] = [str(errors)]
return flat
def from_dict(model_cls: type, data: dict, validate: bool = True) -> tuple[Any, dict]:
"""
Instantiate a schematics model from a dict.
Returns (model_instance, {}) or (None, errors_dict).
"""
try:
m = model_cls(data)
if validate:
m.validate()
return m, {}
except DataError as e:
return None, _flatten_errors(e.errors)
def to_dict(model: Model, role: str | None = None) -> dict:
"""Serialize a model to a plain dict using optional role."""
return model.to_primitive(role=role)
def bulk_validate(model_cls: type, items: list[dict]) -> tuple[list, dict[int, dict]]:
"""
Validate a list of dicts as model instances.
Returns (valid_models, {index: errors}).
"""
valid: list = []
errors: dict[int, dict] = {}
for i, item in enumerate(items):
m, errs = from_dict(model_cls, item)
if errs:
errors[i] = errs
else:
valid.append(m)
return valid, errors
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== User — valid ===")
user, errs = from_dict(User, {
"name": "Alice",
"email": "[email protected]",
"role": "admin",
"password": "secret",
"tags": ["python", "backend"],
})
print("valid:", errs == {})
print("public view:", to_dict(user, role="public"))
print("admin view: ", to_dict(user, role="admin"))
print("minimal: ", to_dict(user, role="minimal"))
print("\n=== User — invalid ===")
_, errs = from_dict(User, {"name": "", "email": "not-an-email", "role": "superuser"})
for field, msgs in errs.items():
print(f" {field}: {msgs}")
print("\n=== Product ===")
prod, errs = from_dict(Product, {
"id": 1,
"name": "Python Dev Kit",
"slug": "python-dev-kit",
"price": 49.99,
"tags": ["python", "tools"],
})
print("product summary:", to_dict(prod, role="summary"))
print("\n=== Order with nested models ===")
order_data = {
"order_id": "ORD-001",
"customer": {"name": "Bob", "email": "[email protected]"},
"items": [
{"product_id": 1, "quantity": 2, "unit_price": 49.99},
],
"shipping": {"street": "123 Main", "city": "Portland", "country": "US"},
}
order, errs = from_dict(Order, order_data)
if not errs:
print(f"order {order.order_id}: {len(order.items)} items")
print(f"line total: ${order.items[0].line_total}")
else:
print("errors:", errs)
print("\n=== bulk_validate ===")
product_rows = [
{"id": 1, "name": "Widget", "slug": "widget", "price": 9.99},
{"id": -1, "name": "", "slug": "Bad Slug", "price": -5},
{"id": 2, "name": "Gadget", "slug": "gadget", "price": 14.99},
]
valid_prods, errs = bulk_validate(Product, product_rows)
print(f"Valid: {len(valid_prods)}, Invalid: {len(errs)}")
for idx, e in errs.items():
print(f" [item {idx}]:", {k: v for k, v in e.items()})
For the attrs/dataclasses alternative — attrs and dataclasses provide structured types with type hints, but validation is not built-in; schematics provides field-level type validation, serialization with roles, coerce-on-assign, and nested model validation out of the box without writing __post_init__ validators. For the pydantic alternative — Pydantic v2 is faster and tighter with Python type annotations; schematics is older but uses explicit field descriptors (StringType, IntType) that are self-documenting, supports MongoDB-style use cases natively, and has a roles system for view-specific serialization that Pydantic doesn’t have built-in. The Claude Skills 360 bundle includes schematics skill sets covering Model class definition, all built-in types (String/Int/Float/Bool/UUID/Email/URL/DateTime/List/Dict/Model), SlugType/PositiveIntType custom field types, validate_model()/from_dict()/to_dict()/bulk_validate() helpers, roles with blacklist/whitelist, nested ModelType/ListType, cross-field model validators, IdentifiedMixin/TimestampedMixin base classes, and User/Product/Order/Address domain models. Start with the free tier to try schematics data model code generation.