Like Pydantic, but in one file. Zero dependencies.
tiny-validator is a single-file, declarative input validation library. No Pydantic, no marshmallow, no attrs. Pure Python β chainable field types, nested schemas, defaults, and structured error reporting.
- πͺΆ Zero dependencies β stdlib
reonly - π¦ Single file β drop
tiny_validator.pyanywhere - π§± Declarative schemas β
Schema({...})withfields.*namespace - π― 12+ field types β string, integer, float, boolean, list, dict, email, URL, UUID, date, datetime, decimal
- πͺ Nested schemas β
fields.Dict(schema=...)for objects - π‘οΈ Strict mode β reject unknown fields
- π Defaults β built into Field
- π¨ Structured errors β
ValidationError.errorsis a list of{path, message} - π Decorator β
@validate(body=..., query=..., headers=...) - 𧬠JSON Schema import β
json_schema()/from_json_schema()for MCP tool contracts - π¦ Batch validation β
schema.validate_many(items)splits valid/invalid - π©Ή Partial validation β
schema.partial()ignores unknown fields
from tiny_validator import Schema, fields, ValidationError, validate
user_schema = Schema({
"name": fields.String(min_length=1, max_length=100),
"email": fields.Email(),
"age": fields.Integer(min_value=0, max_value=150, required=False),
"role": fields.String(choices=["admin", "user"]),
}, strict=True)
try:
clean = user_schema({"name": "Hussain", "email": "h@x.co", "role": "admin"})
except ValidationError as exc:
print(exc.errors)from tiny_validator import AsyncField, AsyncValidator, fields
async def check_repo(value: str) -> str | None:
"""Return None if valid, error string if not."""
exists = await github_api.repo_exists(value)
return None if exists else "repository not found"
schema = AsyncValidator({
"repo": AsyncField(validator_fn=check_repo),
"limit": fields.Integer(default=10),
})
errors = await schema.validate({"repo": "owner/name"})
# [] if repo exists, else [{"path": "repo", "message": "repository not found"}]
# Raises ValidationError like Schema
result = await schema({"repo": "owner/name"})For MCP tools and external contracts that already speak JSON Schema:
from tiny_validator import json_schema, from_json_schema
# From a dict
js = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"age": {"type": "integer", "minimum": 0},
"email": {"type": "string", "format": "email"},
},
"required": ["name", "email"],
"additionalProperties": False,
}
validator = json_schema(js)
errors = validator.validate({"name": "Alice", "email": "alice@example.com"})
# From a JSON string
schema = from_json_schema('{"type": "object", "properties": {"x": {"type": "integer"}}}')Validate a list of items and split into valid/invalid buckets:
from tiny_validator import Schema, fields
schema = Schema({
"name": fields.String(min_length=1),
"email": fields.Email(),
})
result = schema.validate_many([
{"name": "Alice", "email": "alice@example.com"},
{"name": "", "email": "bad"},
])
# result["valid"] = [first_item]
# result["invalid"] = [{"item": bad_item, "errors": [...]}]Ignore unknown fields when the source might include extras (e.g. webhook payloads):
from tiny_validator import Schema, fields
schema = Schema({"name": fields.String(min_length=1)})
partial = schema.partial()
errors = partial.validate({"name": "Alice", "extra": "ignored"})
# errors == [] (extra field is dropped)@validate(
body=Schema({"query": fields.String(min_length=3), "limit": fields.Integer(default=10)}),
query=Schema({"lang": fields.String(choices=["en", "ar"], required=False)}),
)
def search_handler(body, query):
return {"q": body["query"], "limit": body["limit"]}| Field | Common kwargs |
|---|---|
String |
min_length, max_length, pattern, choices, case_insensitive |
Integer |
min_value, max_value, choices |
Float |
min_value, max_value, allow_int |
Boolean |
β |
AsyncField(validator_fn) |
required, default β async field with await-able validator |
List(item_field=β¦) |
min_length, max_length, unique |
Dict(schema=β¦) |
required (alias Object) |
Email / Url / Uuid |
(all extend String) |
Date / DateTime |
(ISO-8601 strings) |
Decimal |
min_value, max_value, max_digits, decimal_places |
Any |
allow_none=True by default |
All fields accept required=True/False and default=....
exc.errors == [
{"path": "user.email", "message": "value must be a valid email address"},
{"path": "user.age", "message": "value must be <= 150"},
]
exc.to_dict() == {
"error": "validation_failed",
"details": [
{"path": "user.email", "message": "value must be a valid email address"},
...
],
}tiny-validator works well for agent-built services where bad inputs should fail early and explain themselves:
- Webhook payload gates β validate provider callbacks before they reach handlers.
- Tool-call contracts β check JSON arguments before running filesystem, browser, or GitHub actions.
- Bounty repro inputs β keep proof-of-concept scripts honest with strict, readable schemas.
- Config and env checks β validate generated config dictionaries before a cron job starts.
Pair it with tiny-router for HTTP endpoints, tiny-cli for operator commands, and tiny-secret when validation errors must avoid leaking sensitive values.
Use from_json_schema() when a tool, MCP server, or generated config already
describes its arguments as JSON Schema:
from tiny_validator import from_json_schema
schema = from_json_schema({
"type": "object",
"required": ["repo", "limit"],
"properties": {
"repo": {"type": "string", "pattern": r"^[^/]+/[^/]+$"},
"limit": {"type": "integer", "minimum": 1, "maximum": 20},
"include_closed": {"type": "boolean", "default": False},
},
"additionalProperties": False,
})
args = schema({"repo": "hussain-alsaibai/tiny-validator", "limit": 5})The bridge intentionally supports a practical subset and raises ValueError
for unsupported keywords, so tool-call validation fails loudly instead of
silently accepting a partial contract.
See also:
- Tool-Call Contracts With tiny-validator β validating agent tool arguments before filesystem, browser, GitHub, or cloud side effects.
- Agent Output Guards With tiny-validator β checking generated plans, patches, and tool payloads before they reach side-effecting tools.
- JSON Schema Tool Contracts With tiny-validator β validating MCP-style tool arguments with a zero-dependency schema bridge.
- Schema Drift Repair Loops With tiny-validator β turning validation failures into safe, inspectable repair prompts for agent outputs and tool payloads.
- Agent Capability Contracts With tiny-validator β strict schemas for MCP tools, GitHub actions, cron jobs, and other side-effecting capabilities.
| Feature | tiny-validator | Pydantic v2 | marshmallow |
|---|---|---|---|
| Dependencies | 0 | 0 (core) | 0 (core) |
| File count | 1 | multiple | multiple |
| Type-driven | β (declarative) | β | β |
| Strict mode | β | β | β |
| Nested schemas | β | β | β |
| Decorator | β | (depends) | β |
| Email / URL / UUID | β | β (str + format) | needs extra |
| Startup time | <10 ms | ~200 ms | ~50 ms |
Use tiny-validator when you want fast, transparent, no-surprise validation, and you don't need Pydantic's serialization/deserialization, model export, or computed fields.
python test_tiny_validator.py -vValidation for the Model Context Protocol server-manifest format lives in the opt-in sidecar module tiny_validator_mcp.py. The core package stays at zero dependencies; importing the sidecar only pulls in tiny_validator (already a dependency).
from tiny_validator import Schema, String
from tiny_validator_mcp import McpServer, McpTool, validate_mcp_manifest
server = McpServer(
name="my-mcp",
version="0.1.0",
title="My MCP Server",
tools=[McpTool(name="echo", description="Echo the input back to the caller.",
input_schema=Schema({"text": String(min_length=1)}))],
metrics=["mcp_my_mcp_calls_total", "mcp_my_mcp_latency_seconds_bucket"],
)
errors = validate_mcp_manifest(server.to_dict())
assert not errorsThe sidecar validates three layers: (1) structural via SERVER_SCHEMA, (2) per-element via TOOL_SCHEMA/RESOURCE_SCHEMA/PROMPT_SCHEMA, (3) metric labels via metric_label_check, enforcing the de-facto mcp_<server>_<unit>_(total|bucket|sum|count|created|info) pattern.
See tiny_validator_mcp.py for the full API and test_v050_mcp.py for 39 tests covering it.
Part of the tiny-* zero-dependency toolkit for Python agent infrastructure:
- tiny-router β HTTP router, 76K req/s
- tiny-log β structured logging
- tiny-validator β input validation, 247K val/s (v0.2.0: JSON Schema import, batch/partial)
- tiny-config β layered config loader (v0.2.0: validation, secrets, profiles)
- tiny-cli β CLI builder with colors
- fast-cache β LRU + TTL + SWR cache
- tiny-rate β rate limiter (token / fixed / sliding)
- tiny-retry β retry + backoff + circuit breaker
- tiny-pool β ThreadPool + AsyncPool
- tiny-agent β zero-dep agent framework
- tiny-mcp
- tiny-mcp-skel - stdlib-only MCP server skeleton (combines tiny-router blueprints + tiny-validator McpServer schema, ships today). β Model Context Protocol
- tiny-embed β embeddings + vector search
- tiny-compose β Stack any decorators in any order, declaratively
- tiny-trace β OTel-compatible tracing, sync + async, W3C propagation
- tiny-secret β Zero-dep secret loader + redacting printer
- tiny-cron β cron-style scheduler + intervals
- tiny-flags β feature flags, percentage rollout
- tiny-queue β persistent FIFO queue, retries
- tiny-metrics β Prometheus-compatible metrics
- tiny-timeout β hard timeouts + cooperative deadlines
- tiny-idempotency β Stripe-style idempotency keys
- tiny-budget β runtime cost + token enforcement for AI agents
- tiny-eventbus β durable pub/sub with JSONL replay
- snapdb β embedded DB
21 repos, ~14,700 LOC, zero dependencies across the entire stack. All single-file, MIT, fully type-hinted. Built by OpenClaw.
MIT β see LICENSE.