Choosing a Validation Approach
Valid8r provides three distinct validation approaches, each optimized for different use cases. This guide helps you select the right approach for your needs.
Decision Flowchart
What are you validating?
|
+---------------+---------------+
| | |
Single value Multiple fields Type annotation
from string? in a dict/form? driven?
| | |
v v v
Parser Chaining Schema Validation from_type()
Quick Decision Tree
Single value from user input? → Use Parser Chaining
Multiple fields that all need validation? → Use Schema Validation
Want type annotations to drive validation? → Use from_type()
Need ALL errors across multiple fields? → Use Schema Validation
Building a CLI with interactive prompts? → Use Parser Chaining with
prompt.ask()Validating API request body? → Use Schema Validation
Working with dataclass-like structures? → Use from_type() with
Annotated
Parser Chaining
Best for: Single values, CLI input, early-exit validation pipelines
Parser chaining uses the Maybe monad’s .bind() method to create validation pipelines that stop at the first failure.
When to Use
Validating individual user inputs (CLI arguments, form fields)
Need fast-fail behavior (stop at first error)
Building interactive prompts with retry logic
Simple validation with 1-3 chained steps
Example
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
# Chain parsing and validation
result = parsers.parse_int("42").bind(
validators.minimum(0)
).bind(
validators.maximum(100)
)
match result:
case Success(value):
print(f"Valid: {value}")
case Failure(error):
print(f"Error: {error}")
Characteristics
Aspect |
Behavior |
|---|---|
Error handling |
Stops at first failure (fail-fast) |
Error count |
Returns single error message |
Input type |
String → parsed type |
Composition |
Via |
Complexity |
Low |
Schema Validation
Best for: Multi-field forms, API request bodies, configuration objects
Schema validation validates entire dict-like objects against a defined structure, collecting ALL validation errors across all fields.
When to Use
Validating API request/response bodies
Form submissions with multiple fields
Configuration file validation
Need ALL errors at once (not just the first)
Complex nested object structures
Want field path tracking (e.g.,
.user.address.street)
Example
from valid8r.core import parsers, schema, validators
from valid8r.core.maybe import Success, Failure
user_schema = schema.Schema(
fields={
'name': schema.Field(
parser=parsers.parse_str,
validator=validators.non_empty_string(),
required=True,
),
'age': schema.Field(
parser=parsers.parse_int,
validator=validators.between(0, 120),
required=True,
),
'email': schema.Field(
parser=parsers.parse_email,
required=True,
),
}
)
result = user_schema.validate({
'name': '',
'age': 'invalid',
'email': 'bad-email',
})
match result:
case Success(data):
print(f"Valid: {data}")
case Failure(errors):
for err in errors:
print(f"{err.path}: {err.message}")
# Output:
# .name: String must not be empty
# .age: Invalid integer format
# .email: Invalid email address
Characteristics
Aspect |
Behavior |
|---|---|
Error handling |
Collects ALL errors across fields |
Error count |
Returns list of ValidationError objects |
Input type |
Dict → validated dict with parsed types |
Composition |
Nested schemas via |
Complexity |
Medium |
from_type() (Type-Based Parsing)
Best for: Type annotation enthusiasts, Python 3.10+ projects, reducing boilerplate
The from_type() function automatically generates parsers from Python type annotations.
When to Use
Strong preference for type-driven development
Using
typing.Annotatedfor validation metadataWant parsers generated from existing type hints
Building libraries that work with generic types
Need Union types or Literal validation
Example
from typing import Annotated, Optional
from valid8r.core.type_adapters import from_type
from valid8r import validators
# Simple type
int_parser = from_type(int)
result = int_parser('42') # Success(42)
# With validation via Annotated
Age = Annotated[int, validators.minimum(0), validators.maximum(120)]
age_parser = from_type(Age)
result = age_parser('25') # Success(25)
result = age_parser('150') # Failure('Value must be at most 120')
# Complex nested type
from typing import Literal
Status = Literal['active', 'inactive', 'pending']
parser = from_type(dict[str, list[Status]])
result = parser('{"users": ["active", "pending"]}')
# Success({'users': ['active', 'pending']})
Characteristics
Aspect |
Behavior |
|---|---|
Error handling |
Stops at first failure |
Error count |
Returns single error message |
Input type |
String (often JSON) → typed value |
Composition |
Via type annotations ( |
Complexity |
Low-Medium |
Comparison Table
Feature |
Parser Chaining |
Schema Validation |
from_type() |
|---|---|---|---|
Input format |
Single string |
Dict/object |
String (JSON for collections) |
Error collection |
First error only |
All errors |
First error only |
Field paths |
N/A |
Yes ( |
N/A |
Nested structures |
Manual chaining |
Nested schemas |
Type annotations |
Type inference |
Explicit parsers |
Explicit parsers |
From type hints |
Validators |
Via |
Via |
Via |
Learning curve |
Low |
Medium |
Low-Medium |
Best for |
CLI, single values |
APIs, forms |
Type-driven code |
Python version |
3.11+ |
3.11+ |
3.11+ (best with 3.12+) |
When to Use Each Approach
Use Parser Chaining When:
Building CLI applications with
prompt.ask()Validating one value at a time
Want simple, readable validation pipelines
Performance matters (no dict overhead)
Need custom error handling per step
# Perfect for CLI prompts
from valid8r import parsers, validators, prompt
age = prompt.ask(
"Enter your age: ",
parser=parsers.parse_int,
validator=validators.between(0, 120),
retry=True
)
Use Schema Validation When:
Validating API request bodies
Need to show ALL validation errors to users
Have complex nested data structures
Want field path tracking for error reporting
Building form validation
Need strict mode to reject extra fields
# Perfect for API validation
result = user_schema.validate(request.json)
match result:
case Failure(errors):
return Response({
'errors': [
{'field': err.path, 'message': err.message}
for err in errors
]
}, status=400)
Use from_type() When:
Already have type annotations you want to leverage
Prefer declarative, type-driven code
Using
Annotatedfor validation metadataNeed Union or Literal type validation
Building generic, reusable components
# Perfect for type-driven validation
from typing import Annotated, Literal
Status = Literal['draft', 'published', 'archived']
Priority = Annotated[int, validators.between(1, 5)]
Task = dict[str, Union[Status, Priority]]
parser = from_type(Task)
Migration Guide
From Parser Chaining to Schema
When you need error accumulation across multiple fields:
Before (Parser Chaining):
# Validates one at a time, stops at first error
name = parsers.parse_str(data['name']).bind(validators.non_empty_string())
age = parsers.parse_int(data['age']).bind(validators.minimum(0))
email = parsers.parse_email(data['email'])
# Manual error collection
errors = []
if name.is_failure():
errors.append(('name', name.error_or('')))
if age.is_failure():
errors.append(('age', age.error_or('')))
# ... tedious
After (Schema Validation):
# Collects all errors automatically
user_schema = schema.Schema(
fields={
'name': schema.Field(
parser=parsers.parse_str,
validator=validators.non_empty_string(),
required=True,
),
'age': schema.Field(
parser=parsers.parse_int,
validator=validators.minimum(0),
required=True,
),
'email': schema.Field(parser=parsers.parse_email, required=True),
}
)
result = user_schema.validate(data)
# All errors in result.error_or([])
From Schema to from_type()
When you want type annotations to drive validation:
Before (Schema):
config_schema = schema.Schema(
fields={
'port': schema.Field(
parser=parsers.parse_int,
validator=validators.between(1024, 65535),
required=True,
),
'debug': schema.Field(parser=parsers.parse_bool, required=False),
}
)
After (from_type):
from typing import Annotated, TypedDict
Port = Annotated[int, validators.between(1024, 65535)]
# Note: TypedDict not directly supported, but you can use dict
parser = from_type(dict[str, Union[Port, bool]])
# Or define individual field parsers from types
port_parser = from_type(Port)
debug_parser = from_type(bool)
From from_type() to Parser Chaining
When you need more control over the validation pipeline:
Before (from_type):
parser = from_type(Annotated[int, validators.minimum(0)])
result = parser(user_input)
After (Parser Chaining):
# More explicit, easier to debug
result = parsers.parse_int(user_input).bind(validators.minimum(0))
Frequently Asked Questions
Which approach is fastest?
Parser Chaining is fastest for single values (no dict/schema overhead). For multi-field validation, all approaches have similar performance since parsing is typically the bottleneck.
Can I combine approaches?
Yes! You can use from_type() to create parsers and use them in Schema fields:
from valid8r.core.type_adapters import from_type
Port = Annotated[int, validators.between(1024, 65535)]
config_schema = schema.Schema(
fields={
'port': schema.Field(
parser=from_type(Port),
required=True,
),
}
)
How do I validate nested objects?
Schema Validation is best for nested objects with error accumulation:
address_schema = schema.Schema(
fields={
'street': schema.Field(parser=parsers.parse_str, required=True),
'city': schema.Field(parser=parsers.parse_str, required=True),
}
)
user_schema = schema.Schema(
fields={
'name': schema.Field(parser=parsers.parse_str, required=True),
'address': schema.Field(
parser=address_schema.validate, # Nest schemas!
required=True,
),
}
)
What if I need custom error messages?
All approaches support custom error messages:
Parser Chaining:
parsers.parse_int("abc", error_message="Please enter a number")
validators.minimum(0, "Age cannot be negative")
Schema:
schema.Field(
parser=parsers.parse_int,
validator=validators.minimum(0, "Age cannot be negative"),
required=True,
)
from_type:
# Validators in Annotated include error messages
Annotated[int, validators.minimum(0, "Age cannot be negative")]
Should I use Schema or Pydantic for APIs?
Pydantic is better for FastAPI integration (native support, OpenAPI generation)
Schema is better when you want Maybe monad error handling and don’t need Pydantic’s features
See the Library Comparison Guide for detailed comparisons.
Summary
Scenario |
Recommended Approach |
|---|---|
CLI with interactive prompts |
Parser Chaining |
Single value validation |
Parser Chaining |
API request body |
Schema Validation |
Form with multiple fields |
Schema Validation |
Need ALL errors at once |
Schema Validation |
Type-annotation driven |
from_type() |
Annotated validators |
from_type() |
Union/Literal types |
from_type() |
When in doubt:
Start with Parser Chaining for simplicity
Move to Schema Validation when you need error accumulation
Use from_type() when type annotations are central to your design