Files
cleveragents-core/docs/adr/ADR-004-data-validation.md
freemo c2db74ba81
CI / lint (push) Successful in 15s
CI / build (push) Successful in 16s
CI / quality (push) Successful in 20s
CI / security (push) Successful in 35s
CI / typecheck (push) Successful in 42s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m38s
CI / integration_tests (push) Successful in 3m11s
CI / docker (push) Successful in 39s
CI / coverage (push) Successful in 4m58s
CI / benchmark-publish (push) Successful in 17m32s
Docs: Restyled ADR pages
2026-03-10 12:38:35 -04:00

7.3 KiB

adr_number, title, status_history, tier, authors, superseded_by, related_adrs, acceptance
adr_number title status_history tier authors superseded_by related_adrs acceptance
4 Data Validation
2026-02-16
Proposed
Jeffrey Phillips Freeman
2026-02-16
Accepted
Jeffrey Phillips Freeman
1
Jeffrey Phillips Freeman
null
number title relationship
1 Layered Architecture Pydantic models enforce Domain Layer contracts at layer boundaries
number title relationship
5 Technical Stack Specifies Pydantic V2 as the chosen validation library
number title relationship
11 Tool System Tool input/output schemas are Pydantic models that generate JSON Schema
number title relationship
24 Configuration System Pydantic Settings powers environment variable loading and config validation
votes_for votes_against abstentions
voter comment
Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com> Pydantic V2 strict mode gives us runtime type safety with minimal boilerplate and native JSON Schema support

Context

CleverAgents processes data from multiple untrusted or semi-trusted sources: user CLI input, YAML configuration files, LLM-generated tool call arguments, MCP server responses, environment variables, and database records. Every boundary crossing — between layers, between the system and external services, between the user and the application — is an opportunity for invalid data to enter the system and cause subtle downstream failures.

The system needs a uniform validation mechanism that provides runtime type checking, schema generation (for tool calling and API contracts), serialization/deserialization, and clear error messages — without requiring developers to write manual validation logic for each data boundary.

Decision Drivers

  • Data enters from multiple untrusted sources: user CLI input, LLM-generated tool arguments, MCP responses, YAML configs, environment variables
  • Every layer boundary crossing is an opportunity for invalid data to cause subtle downstream failures
  • Tool-calling protocols require JSON Schema definitions generated from the same models used for validation
  • Need a single modeling framework for domain models, configuration, API schemas, and tool definitions to avoid duplication
  • Validation error messages must be structured and actionable for CLI display
  • Runtime type checking must be enforced without requiring manual validation logic at each boundary

Decision

All domain models, configuration objects, and API schemas use Pydantic V2 (>= 2.7.0) for runtime data validation, JSON Schema generation, and serialization. Pydantic Settings (>= 2.11.0) extends this to environment variable loading. Pydantic serves as the type-safe data boundary between all layers.

Design

Domain Models

Every domain entity (Plan, Decision, Action, Resource, Actor, Tool, Skill, Session, Invariant, AutomationProfile, Checkpoint, CorrectionAttempt) is a Pydantic BaseModel subclass. Fields use Python type annotations with Pydantic field validators for business rule enforcement.

Configuration Objects

YAML entity configuration files (actors, skills, tools, actions, resource types, automation profiles, context views) are parsed into Pydantic models. Validation errors are reported with field paths and human-readable messages before any processing occurs.

Environment and Global Configuration

BaseSettings subclasses from Pydantic Settings handle environment variable loading with the CLEVERAGENTS_ prefix, .env file support, and nested model resolution. This implements the four-tier configuration resolution chain (CLI flag > environment variable > project-scoped config > global config file > built-in default).

JSON Schema Generation

Pydantic models automatically generate JSON Schema definitions, which are used for:

  • Tool input/output schema definitions (consumed by LLM tool-calling protocols).
  • Validation of LLM-generated tool call arguments before execution.
  • API request/response schema documentation.

Serialization

Pydantic handles serialization to and from JSON, YAML (via dict intermediary), and database column types. Custom serializers are defined for domain-specific types (ULIDs, datetime formats, enum values).

Constraints

  • All data crossing a layer boundary must pass through a Pydantic model. Raw dictionaries or untyped data structures must not propagate between layers.
  • Pydantic's strict mode is used where appropriate to prevent implicit type coercion (e.g., strings silently becoming integers).
  • Configuration YAML files must validate completely before any side effects occur. A partially valid configuration must never be applied.
  • JSON Schema generated from Pydantic models is the canonical schema for tool definitions. Manual schema definitions must not diverge from the model.
  • Validation error messages must include the field path and a human-readable explanation suitable for CLI display.

Consequences

Positive

  • Runtime type safety is enforced at every data boundary without manual validation code.
  • A single modeling framework serves domain models, configuration, API schemas, and tool definitions — reducing duplication.
  • JSON Schema generation eliminates manual schema maintenance for tool calling and API contracts.
  • Pydantic's error reporting provides structured, actionable validation messages.

Negative

  • Pydantic V2's strict mode can reject data that "looks right" to a developer unfamiliar with the strictness rules (e.g., "123" rejected where 123 is expected).
  • All domain models carry a Pydantic dependency, which is a framework coupling in the domain layer. This is an accepted trade-off given Pydantic's role as a validation primitive rather than an infrastructure framework.
  • Complex nested models with custom validators can be difficult to debug when validation fails deep in the model hierarchy.

Risks

  • If Pydantic's validation performance becomes a bottleneck (e.g., in hot paths during context assembly or decision tree traversal), selective use of model_construct() (bypassing validation) may be needed — but this must be explicitly justified and documented.
  • Pydantic major version upgrades (V2 → future V3) could require model migration effort.

Alternatives Considered

dataclasses + manual validation — Python dataclasses provide structure but no runtime validation, no JSON Schema generation, and no serialization. Every boundary check would require hand-written validation functions, leading to inconsistency and maintenance burden.

attrs + cattrs — A capable alternative, but lacks the JSON Schema generation and Settings integration that Pydantic provides out of the box. The LLM ecosystem (LangChain, MCP) standardizes on Pydantic models, making it the natural choice for interoperability.

Compliance

  • Type checking: Pyright in strict mode verifies that Pydantic model field types are consistent across the codebase.
  • Schema drift tests: Automated tests verify that JSON Schema generated from Pydantic tool models matches the schemas expected by MCP and LangChain tool-calling protocols.
  • Validation coverage: Unit tests exercise validation edge cases — missing required fields, wrong types, out-of-range values, malformed nested objects — for every domain model and configuration object.
  • Code review: New data structures that cross layer boundaries must be Pydantic models. Raw dict usage at boundaries is flagged during review.