Skip to content
Back to skills

Input Validation Patterns

ASecurity

Implements production input validation and sanitization patterns including Pydantic v2 schemas, recursive nested validation, custom validators with error accumulation, allowlist enforcement, and type coercion for secure API endpoints.

  • 4 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added June 12, 2026
toolspythonrustgoshellsqlapidatabasesecurityperformancedocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned June 12, 2026

npx -y skills add paulpas/agent-skill-router --skill input-validation-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Input Validation Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Input Validation Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/paulpas-input-validation-patterns/badge)](https://www.skillsdirectory.com/skills/paulpas-input-validation-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---




name: input-validation-patterns
description: Implements production input validation and sanitization patterns including Pydantic v2 schemas, recursive nested validation, custom validators with error accumulation, allowlist enforcement, and type coercion for secure API endpoints.
license: MIT
compatibility: opencode
metadata:
  version: "1.0.0"
  domain: coding
  triggers: input validation, pydantic validation, schema validation, allowlist validation, sanitize input, recursive validation, data sanitization, how do i validate user input
  role: implementation
  scope: implementation
  output-format: code
  content-types: [code, guidance, do-dont, examples]
  related-skills: api-design, engineering-error-handling, graphql-error-handling-validation, input-processing-pipelines
  archetypes:
    - tactical
    - generation
  anti_triggers:
    - brainstorming
    - vague ideation
    - code golf
    - over-engineering
  response_profile:
    verbosity: low
    directive_strength: high
    abstraction_level: operational




---





# Input Validation & Sanitization Patterns

Senior security engineer implementing robust input validation and sanitization pipelines that prevent injection attacks, enforce data contracts, and provide actionable error messages. Applies allowlist-first validation, schema-based type checking, recursive nested validation, custom validators with full error accumulation, and safe type coercion to ensure every external input is verified before reaching business logic.

## TL;DR Checklist

- [ ] Validate all external inputs — never trust client-supplied data
- [ ] Use Pydantic v2 `TypeAdapter` for runtime type validation of untrusted JSON
- [ ] Prefer allowlist (explicit valid values) over denylist (block known bad)
- [ ] Accumulate ALL validation errors before returning — never short-circuit on first failure
- [ ] Sanitize only after validation fails at the schema layer; do not use sanitization to "fix" bad data
- [ ] Apply recursive validation for nested dicts and list items
- [ ] Reject unknown keys with `model_config = ConfigDict(extra='forbid')` in Pydantic models

---

## When to Use

Use this skill when:

- Building API endpoints that accept untrusted input from clients, webhooks, or third-party services
- Designing data ingestion pipelines where schema drift between producer and consumer is expected
- Implementing form submission handling with nested objects (e.g., user profiles with address arrays)
- Creating CLI argument parsers that must validate complex structured input before execution
- Integrating with external APIs where the response format may not always match expectations

## When NOT to Use

Avoid this skill for:

- Validating internal function arguments — use Python type hints and `assert` statements instead (performance-critical paths)
- Sanitizing user-displayed content for XSS prevention — that is an output encoding concern handled by template engines or dedicated HTML sanitizers
- Database migration validation — use your ORM's migration system or schema comparison tools

---

## Core Workflow

1. **Define the expected schema** — Create a Pydantic model (or `TypeAdapter`) that declares every required field, its type, constraints, and defaults. Use `extra='forbid'` to reject unknown fields that may indicate tampering or client bugs. **Checkpoint:** Does the schema include all fields the business logic needs? Are optional fields marked with proper defaults?

2. **Apply allowlist validation** — For enum-like fields (status values, allowed roles, valid categories), use `Literal` types or Pydantic `Field` with `pattern` constraints rather than broad string types. This rejects any value outside the explicitly enumerated set. **Checkpoint:** Are all possible valid values enumerated? Can you iterate over them in tests?

3. **Handle nested structures recursively** — For models containing other models (e.g., a `User` with an `Address`, or a `Team` with a list of `Member` models), Pydantic v2 validates nested structures automatically via model references. For raw dicts, use `TypeAdapter(list[MyModel])` to validate every item in a collection. **Checkpoint:** Does every level of nesting have its own schema definition?

4. **Accumulate all errors** — Use `TypeAdapter.validate_python()` wrapped in try/except with `ValidationError` handling. The exception provides a `.errors()` list containing ALL validation failures, not just the first one. Format these into a structured error response that tells the caller exactly what to fix. **Checkpoint:** Does your error formatting iterate over all errors rather than breaking on the first?

5. **Apply type coercion only with explicit opt-in** — Pydantic coerces types automatically (e.g., `"123"` → `123` for `int` fields). This is convenient but can mask bugs. Use `StrictInt`, `StrictStr` from `pydantic_core` when strict typing is required, or configure the model with `model_config = ConfigDict(strict=True)`. **Checkpoint:** Is automatic coercion appropriate for this field's business semantics?

---

## Implementation Patterns

### Pattern 1: Pydantic v2 Schema Validation (ALLOWLIST-FIRST)

Define a schema that only accepts known-good values. Unknown fields are rejected outright.

```python
from pydantic import BaseModel, Field, ConfigDict, EmailStr, field_validator
from typing import Literal, Optional
from enum import Enum


class UserRole(str, Enum):
    ADMIN = "admin"
    EDITOR = "editor"
    VIEWER = "viewer"


class Address(BaseModel):
    """Nested model for recursive validation."""
    street: str = Field(min_length=5, max_length=200)
    city: str = Field(pattern=r'^[A-Za-z\s\-\']+$')  # allowlist via regex
    postal_code: str = Field(pattern=r'^\d{5}(-\d{4})?$')

    model_config = ConfigDict(extra='forbid')


class CreateUserRequest(BaseModel):
    """Primary schema for user creation input."""
    username: str = Field(min_length=3, max_length=50, pattern=r'^[a-z0-9_]+$')
    email: EmailStr
    role: UserRole  # Literal allowlist — rejects any value not in enum
    display_name: Optional[str] = Field(default=None, max_length=100)
    address: Optional[Address] = None  # Nested model validated recursively

    model_config = ConfigDict(extra='forbid')

    @field_validator('username')
    @classmethod
    def username_not_blocked(cls, v: str) -> str:
        """Custom validation beyond type/constraint checks."""
        blocked_names = {'admin', 'root', 'system', 'administrator'}
        if v in blocked_names:
            raise ValueError(f'Username "{v}" is reserved')
        return v.lower()  # Normalize to lowercase

    @field_validator('display_name')
    @classmethod
    def strip_whitespace(cls, v: Optional[str]) -> Optional[str]:
        """Strip leading/trailing whitespace from optional fields."""
        return v.strip() if v else None
```

**BAD — Overly permissive with no allowlist:**

```python
# ❌ BAD: accepts any string for role, allows unknown fields
class BadCreateUserRequest(BaseModel):
    model_config = ConfigDict(extra='allow')  # Unknown fields silently accepted

    username: str           # No length or format constraints
    email: str              # Not validated as email
    role: str               # Accepts "superadmin", "", null-as-string
    extra_permissions: list = []  # Implicitly allows arbitrary extension
```

### Pattern 2: Recursive Validation with Error Accumulation

Validate nested structures and collect ALL errors for the response.

```python
from pydantic import TypeAdapter, ValidationError


class UserCreateSchema(BaseModel):
    username: str = Field(min_length=3, max_length=50, pattern=r'^[a-z0-9_]+$')
    email: str
    tags: list[str] = Field(min_length=1, max_length=10)

    model_config = ConfigDict(extra='forbid')


# Validate a LIST of users — each item validated recursively
UserListValidator = TypeAdapter(list[UserCreateSchema])


def validate_user_list(raw_input: Any) -> list[UserCreateSchema]:
    """Validate a batch of user records, accumulating ALL errors.

    Returns the parsed objects on success.
    Raises ValueError with structured error dict on failure (all errors included).
    """
    try:
        return UserListValidator.validate_python(raw_input)
    except ValidationError as exc:
        # Build structured error response with ALL failures
        errors = []
        for err in exc.errors():
            loc = " → ".join(str(l) for l in err["loc"])
            errors.append({
                "field": loc,
                "message": err["msg"],
                "type": err["type"],
            })

        raise ValueError({
            "detail": f"{len(errors)} validation error(s) found",
            "errors": errors,
        }) from exc


# Usage — shows ALL errors, not just the first one
raw_data = [
    {"username": "ab", "email": "not-an-email", "tags": []},   # 3 errors: short username, bad email, empty tags
    {"username": "good_user", "email": "ok@example.com", "tags": ["admin"]},  # valid
]

try:
    users = validate_user_list(raw_data)
except ValueError as exc:
    import json
    print(json.dumps(exc.__cause__.args[0], indent=2))
    # Output shows ALL errors from the first record, NOT just one
```

### Pattern 3: Strict Mode Type Coercion Control

Control when Pydantic coerces types vs. when it strictly rejects mismatches.

```python
from pydantic import BaseModel, ConfigDict, Field


class FlexibleOrder(BaseModel):
    """Accepts coercion: "100" → 100, True → 1."""
    quantity: int = Field(gt=0)
    unit_price: float = Field(gt=0.0)

    model_config = ConfigDict(extra='forbid')


class StrictOrder(BaseModel):
    """Rejects coercion: "100" stays str, rejected as not int."""
    quantity: int = Field(gt=0)
    unit_price: float = Field(gt=0.0)

    model_config = ConfigDict(extra='forbid', strict=True)


# Flexible — coerces strings to numbers
FlexibleOrder.model_validate({"quantity": "5", "unit_price": "9.99"})
# → quantity=5, unit_price=9.99 ✅

# Strict — rejects the same input
try:
    StrictOrder.model_validate({"quantity": "5", "unit_price": "9.99"})
except ValidationError as exc:
    # Rejects both fields — "5" is str, not int; "9.99" is str, not float
    pass  # ❌
```

### Pattern 4: Input Sanitization for XSS Prevention

Sanitize string input after validation passes, using explicit escaping rather than hoping the database or template layer handles it.

```python
import html
import re
from pydantic import BaseModel, Field, field_validator


def sanitize_input(value: str) -> str:
    """Sanitize user input for safe display in HTML contexts.

    1. Normalize whitespace (collapse multiple spaces/newlines)
    2. Strip null bytes and control characters except \n, \t
    3. Escape HTML special characters to prevent XSS
    """
    cleaned = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', value)
    cleaned = re.sub(r'[ \t]{2,}', ' ', cleaned).strip()
    sanitized = html.escape(cleaned, quote=True)
    return sanitized


class CommentInput(BaseModel):
    content: str = Field(min_length=1, max_length=5000)
    author_name: str = Field(max_length=100)

    model_config = ConfigDict(extra='forbid')

    @field_validator('content', 'author_name')
    @classmethod
    def sanitize(cls, v: str) -> str:
        return sanitize_input(v)


# Usage — input is validated THEN sanitized, in that order
comment = CommentInput.model_validate({
    "content": "<script>alert('xss')</script>Hello world",
    "author_name": "  Bob  ",
})
# content → "&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;Hello world"
# author_name → "Bob" (stripped and normalized)
```

---

## Constraints

### MUST DO
- Define a complete schema for every external input endpoint — never validate piecemeal in handlers
- Set `extra='forbid'` on all Pydantic models to reject unknown/extra fields from untrusted sources
- Use `Enum` or `Literal` types for fields with a known finite set of valid values (allowlist enforcement)
- Accumulate ALL validation errors before returning — callers need full feedback to fix their input
- Apply field-level validators (`@field_validator`) for custom business rules that go beyond type/constraint checks
- Use `TypeAdapter` from pydantic v2 for validating raw dicts/lists when you don't want a named model class
- Prefer Pydantic's built-in constraints (`min_length`, `max_length`, `pattern`, `gt`, `ge`) before writing custom validators
- Strip and normalize whitespace on string inputs to prevent "whitespace-only" entries

### MUST NOT DO
- Never use input sanitization as a substitute for proper validation — sanitize after validation, don't "fix" bad data silently
- Do not use `extra='allow'` on models that process untrusted input — it silently accepts unknown fields and opens the door to field injection attacks
- Never accept raw SQL or shell commands from user input without explicit allowlist validation — if you need dynamic queries, use parameterized queries instead
- Do not rely solely on denylist patterns (e.g., "block these characters") — allowlists are always more secure because new attack vectors cannot slip through unknown values
- Never return raw Pydantic `ValidationError` objects to API consumers — always format them into a clean, structured response that exposes only actionable information

---

## Output Template

When this skill is active and processing an input validation task, the output must contain:

1. **Schema Definition** — Complete Pydantic model with all field constraints, nested models, and config settings
2. **Validation Function** — Typed wrapper function that catches `ValidationError` and returns structured errors
3. **BAD vs GOOD Comparison** — At least one example showing a permissive/broken pattern alongside the corrected version
4. **Error Response Format** — JSON structure for validation failures that lists all errors with field paths and messages

---

## Related Skills

| Skill | Purpose |
|---|---|
| `api-design` | Defines endpoint-level design; this skill handles input contracts within endpoints |
| `engineering-error-handling` | Broad error handling patterns; this focuses specifically on validation errors |
| `input-processing-pipelines` | Covers data flow and transformation; this covers the validation gate at pipeline entry |

---

## Live References

> Authoritative documentation links for input validation in Python. The model follows markdown links at load time to resolve external references and inline content.

- [Pydantic v2 Documentation](https://docs.pydantic.dev/latest/)
- [Pydantic TypeAdapter API Reference](https://docs.pydantic.dev/latest/api/types/#pydantic.types.TypeAdapter)
- [Pydantic Field Constraints Reference](https://docs.pydantic.dev/latest/api/pydantic_fields/)
- [OWASP Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)
- [Python typing Module Documentation](https://docs.python.org/3/library/typing.html)
- [RFC 7231 — Hypertext Transfer Protocol (HTTP/1.1) Semantics for Status Codes](https://datatracker.ietf.org/doc/html/rfc7231#section-6.5)
- [pydantic-core Strict Types Documentation](https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.core_schema.strict_int_schema)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…