Skip to content

Validate API Reference

Auto-generated API documentation for the validate module.

validate

Zero-dependency runtime validator for TypedDict and dataclass types.

Part of zerodep: https://github.com/Oaklight/zerodep Copyright (c) 2026 Peng Ding. MIT License.

Validate arbitrary data against stdlib type annotations (TypedDict, dataclass, Annotated constraints) and generate JSON Schema from the same type definitions.

Basic usage::

from validate import validate, json_schema, ValidationError

class User(TypedDict):
    name: str
    age: int

validate({"name": "Alice", "age": 30}, User)   # ok
validate({"name": "Alice", "age": "x"}, User)   # raises ValidationError

schema = json_schema(User)  # {"type": "object", "properties": ...}

Annotated constraints::

from validate import Gt, MinLen
from typing import Annotated

class Item(TypedDict):
    name: Annotated[str, MinLen(1)]
    price: Annotated[float, Gt(0)]

Field validators (transform + validate)::

from validate import FieldValidator

def strip_lower(v: str) -> str:
    v = v.strip().lower()
    if not v:
        raise ValueError("must not be empty")
    return v

class User(TypedDict):
    name: Annotated[str, FieldValidator(strip_lower)]

Model validators (cross-field validation)::

from validate import model_validator

class RegisterForm(TypedDict):
    password: str
    confirm: str

@model_validator(RegisterForm)
def passwords_match(data: dict) -> dict:
    if data["password"] != data["confirm"]:
        raise ValueError("passwords do not match")
    return data

Gt dataclass

Value must be strictly greater than val.

Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class Gt:
    """Value must be strictly greater than *val*."""

    val: float

    def check(self, value: Any) -> bool:
        return value > self.val

    def schema_kw(self) -> dict[str, Any]:
        return {"exclusiveMinimum": self.val}

    def __str__(self) -> str:
        return f"> {self.val}"

Ge dataclass

Value must be greater than or equal to val.

Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class Ge:
    """Value must be greater than or equal to *val*."""

    val: float

    def check(self, value: Any) -> bool:
        return value >= self.val

    def schema_kw(self) -> dict[str, Any]:
        return {"minimum": self.val}

    def __str__(self) -> str:
        return f">= {self.val}"

Lt dataclass

Value must be strictly less than val.

Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class Lt:
    """Value must be strictly less than *val*."""

    val: float

    def check(self, value: Any) -> bool:
        return value < self.val

    def schema_kw(self) -> dict[str, Any]:
        return {"exclusiveMaximum": self.val}

    def __str__(self) -> str:
        return f"< {self.val}"

Le dataclass

Value must be less than or equal to val.

Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class Le:
    """Value must be less than or equal to *val*."""

    val: float

    def check(self, value: Any) -> bool:
        return value <= self.val

    def schema_kw(self) -> dict[str, Any]:
        return {"maximum": self.val}

    def __str__(self) -> str:
        return f"<= {self.val}"

MinLen dataclass

Length must be at least val.

Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class MinLen:
    """Length must be at least *val*."""

    val: int

    def check(self, value: Any) -> bool:
        return len(value) >= self.val

    def schema_kw(self) -> dict[str, Any]:
        if isinstance(self.val, int):
            return {"minLength": self.val}
        return {"minLength": self.val}

    def schema_kw_array(self) -> dict[str, Any]:
        return {"minItems": self.val}

    def __str__(self) -> str:
        return f"len >= {self.val}"

MaxLen dataclass

Length must be at most val.

Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class MaxLen:
    """Length must be at most *val*."""

    val: int

    def check(self, value: Any) -> bool:
        return len(value) <= self.val

    def schema_kw(self) -> dict[str, Any]:
        return {"maxLength": self.val}

    def schema_kw_array(self) -> dict[str, Any]:
        return {"maxItems": self.val}

    def __str__(self) -> str:
        return f"len <= {self.val}"

Match dataclass

Value must match pattern (via re.fullmatch).

Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class Match:
    """Value must match *pattern* (via ``re.fullmatch``)."""

    pattern: str

    def check(self, value: Any) -> bool:
        return re.fullmatch(self.pattern, value) is not None

    def schema_kw(self) -> dict[str, Any]:
        return {"pattern": self.pattern}

    def __str__(self) -> str:
        return f"match({self.pattern!r})"

Predicate dataclass

Value must satisfy a custom predicate function.

Parameters:

Name Type Description Default
fn Callable[[Any], bool]

A callable (value) -> bool.

required
description str

Human-readable description for error messages.

'custom predicate'
Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class Predicate:
    """Value must satisfy a custom predicate function.

    Args:
        fn: A callable ``(value) -> bool``.
        description: Human-readable description for error messages.
    """

    fn: Callable[[Any], bool]
    description: str = "custom predicate"

    def check(self, value: Any) -> bool:
        return self.fn(value)

    def schema_kw(self) -> dict[str, Any]:
        return {}

    def __str__(self) -> str:
        return self.description

FieldValidator dataclass

Custom validator that can transform the field value.

Unlike Predicate (which returns bool), the function receives the validated value and returns a (possibly transformed) value. Raise ValueError or AssertionError to signal failure.

Parameters:

Name Type Description Default
fn Callable[[Any], Any]

A callable (value) -> value. Raise on failure.

required
description str

Human-readable description for error messages.

'custom validator'
Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class FieldValidator:
    """Custom validator that can transform the field value.

    Unlike ``Predicate`` (which returns bool), the function receives the
    validated value and returns a (possibly transformed) value.  Raise
    ``ValueError`` or ``AssertionError`` to signal failure.

    Args:
        fn: A callable ``(value) -> value``.  Raise on failure.
        description: Human-readable description for error messages.
    """

    fn: Callable[[Any], Any]
    description: str = "custom validator"

    def validate(self, value: Any) -> Any:
        return self.fn(value)

    def schema_kw(self) -> dict[str, Any]:
        return {}

    def __str__(self) -> str:
        return self.description

Doc dataclass

Field description marker for Annotated types.

Attaches a human-readable description to a type annotation. The description is emitted as {"description": ...} in JSON Schema and has no effect on validation.

Example::

Annotated[str, Doc("The user's full name")]
Source code in validate/validate.py
@dataclasses.dataclass(frozen=True, slots=True)
class Doc:
    """Field description marker for ``Annotated`` types.

    Attaches a human-readable description to a type annotation.  The
    description is emitted as ``{"description": ...}`` in JSON Schema
    and has no effect on validation.

    Example::

        Annotated[str, Doc("The user's full name")]
    """

    description: str

    def check(self, value: Any) -> bool:
        return True

    def schema_kw(self) -> dict[str, Any]:
        return {"description": self.description}

    def __str__(self) -> str:
        return self.description

ErrorDetail dataclass

A single validation error.

Attributes:

Name Type Description
path str

Dotted/bracketed path to the failing field (e.g. "items[2].name").

expected str

Expected type or constraint description.

actual str

Actual type or value description.

message str

Human-readable error message.

Source code in validate/validate.py
@dataclasses.dataclass
class ErrorDetail:
    """A single validation error.

    Attributes:
        path: Dotted/bracketed path to the failing field (e.g. ``"items[2].name"``).
        expected: Expected type or constraint description.
        actual: Actual type or value description.
        message: Human-readable error message.
    """

    path: str
    expected: str
    actual: str
    message: str

ValidationError

Bases: Exception

Raised when validation fails.

Attributes:

Name Type Description
errors

List of all validation errors found.

Source code in validate/validate.py
class ValidationError(Exception):
    """Raised when validation fails.

    Attributes:
        errors: List of all validation errors found.
    """

    def __init__(self, errors: list[ErrorDetail]) -> None:
        self.errors = errors
        msgs = "; ".join(e.message for e in errors[:5])
        if len(errors) > 5:
            msgs += f" ... and {len(errors) - 5} more"
        super().__init__(f"{len(errors)} validation error(s): {msgs}")

model_validator(tp)

Register a model-level validator for a TypedDict or dataclass type.

The validator receives the full data dict after all field validation passes. It should return the (possibly modified) dict, or raise ValueError / AssertionError on failure.

Parameters:

Name Type Description Default
tp type

The TypedDict or dataclass type to attach the validator to.

required

Returns:

Type Description
Callable[[Callable], Callable]

A decorator that registers the function and returns it unchanged.

Example::

class RegisterForm(TypedDict):
    password: str
    confirm: str

@model_validator(RegisterForm)
def passwords_match(data: dict) -> dict:
    if data["password"] != data["confirm"]:
        raise ValueError("passwords do not match")
    return data
Source code in validate/validate.py
def model_validator(tp: type) -> Callable[[Callable], Callable]:
    """Register a model-level validator for a TypedDict or dataclass type.

    The validator receives the full data dict after all field validation
    passes.  It should return the (possibly modified) dict, or raise
    ``ValueError`` / ``AssertionError`` on failure.

    Args:
        tp: The TypedDict or dataclass type to attach the validator to.

    Returns:
        A decorator that registers the function and returns it unchanged.

    Example::

        class RegisterForm(TypedDict):
            password: str
            confirm: str

        @model_validator(RegisterForm)
        def passwords_match(data: dict) -> dict:
            if data["password"] != data["confirm"]:
                raise ValueError("passwords do not match")
            return data
    """

    def decorator(fn: Callable) -> Callable:
        _MODEL_VALIDATORS.setdefault(tp, []).append(fn)
        return fn

    return decorator

validate(data, tp, *, coerce=False)

Validate data against type annotation tp.

Parameters:

Name Type Description Default
data Any

The data to validate.

required
tp Any

A TypedDict class, dataclass class, or any type annotation.

required
coerce bool

If True, attempt type coercion (e.g. str to int).

False

Returns:

Type Description
Any

The validated (and possibly coerced) data.

Raises:

Type Description
ValidationError

If validation fails, with all errors collected.

Source code in validate/validate.py
def validate(data: Any, tp: Any, *, coerce: bool = False) -> Any:
    """Validate *data* against type annotation *tp*.

    Args:
        data: The data to validate.
        tp: A TypedDict class, dataclass class, or any type annotation.
        coerce: If True, attempt type coercion (e.g. str to int).

    Returns:
        The validated (and possibly coerced) data.

    Raises:
        ValidationError: If validation fails, with all errors collected.
    """
    errors: list[ErrorDetail] = []
    result = _validate(data, tp, "", errors, coerce)
    if errors:
        raise ValidationError(errors)
    return result

json_schema(tp, *, title=None)

Generate a JSON Schema dict from a type annotation.

Parameters:

Name Type Description Default
tp Any

A TypedDict class, dataclass class, or any type annotation.

required
title str | None

Optional title for the schema root.

None

Returns:

Type Description
dict[str, Any]

A JSON Schema dict (draft 2020-12 compatible subset).

Source code in validate/validate.py
def json_schema(tp: Any, *, title: str | None = None) -> dict[str, Any]:
    """Generate a JSON Schema dict from a type annotation.

    Args:
        tp: A TypedDict class, dataclass class, or any type annotation.
        title: Optional title for the schema root.

    Returns:
        A JSON Schema dict (draft 2020-12 compatible subset).
    """
    schema = _type_to_schema(tp)
    if title is not None:
        schema["title"] = title
    elif isinstance(tp, type) and hasattr(tp, "__name__"):
        schema["title"] = tp.__name__
    return schema

create_struct(name, fields)

Dynamically create a TypedDict type from a field specification.

Each field is specified as (type, default) where ... (Ellipsis) marks the field as required. Fields with any other default value are optional (NotRequired).

The returned type works with :func:validate and :func:json_schema just like a hand-written TypedDict. Default values are stored in a __field_defaults__ class attribute and emitted by :func:json_schema.

Parameters:

Name Type Description Default
name str

The name of the new type.

required
fields dict[str, tuple[Any, ...]]

Mapping of field names to (type, default) tuples.

required

Returns:

Type Description
type

A new TypedDict class.

Example::

Params = create_struct("Params", {
    "query": (str, ...),            # required
    "limit": (int, 10),             # optional, default 10
    "tag": (str | None, None),      # optional, default None
})
Source code in validate/validate.py
def create_struct(name: str, fields: dict[str, tuple[Any, ...]]) -> type:
    """Dynamically create a TypedDict type from a field specification.

    Each field is specified as ``(type, default)`` where ``...`` (Ellipsis)
    marks the field as required.  Fields with any other default value are
    optional (``NotRequired``).

    The returned type works with :func:`validate` and :func:`json_schema`
    just like a hand-written ``TypedDict``.  Default values are stored in
    a ``__field_defaults__`` class attribute and emitted by
    :func:`json_schema`.

    Args:
        name: The name of the new type.
        fields: Mapping of field names to ``(type, default)`` tuples.

    Returns:
        A new TypedDict class.

    Example::

        Params = create_struct("Params", {
            "query": (str, ...),            # required
            "limit": (int, 10),             # optional, default 10
            "tag": (str | None, None),      # optional, default None
        })
    """
    import sys

    if sys.version_info >= (3, 11):
        from typing import NotRequired, TypedDict
    else:
        from typing_extensions import NotRequired, TypedDict

    annotations: dict[str, Any] = {}
    defaults: dict[str, Any] = {}
    for field_name, spec in fields.items():
        field_type, default = spec
        if default is ...:
            annotations[field_name] = field_type
        else:
            annotations[field_name] = NotRequired[field_type]
            defaults[field_name] = default

    td = TypedDict(name, annotations)  # type: ignore[operator]  # ty: ignore[invalid-argument-type, mismatched-type-name]
    td.__field_defaults__ = defaults  # type: ignore[attr-defined]  # ty: ignore[unresolved-attribute]
    return td