Building Validation Pipelines
Validation pipelines allow you to chain multiple validation and transformation steps into a single execution sequence. In this tutorial, you will build a currency processing pipeline that cleans raw string input, converts it to a Decimal type, and enforces numerical constraints.
You will use the low-level ChainSchema from pydantic-core and the high-level experimental pipeline API from pydantic.
Prerequisites
To follow this tutorial, you need pydantic and pydantic-core installed. The pipeline API is located in the experimental module:
from pydantic.experimental.pipeline import validate_as, transform
Step 1: Building a Low-Level Chain
The foundation of validation pipelines is the ChainSchema. It executes a list of CoreSchema steps in order, passing the output of one step as the input to the next.
Create a basic validator that ensures an input is a string and then appends a suffix using pydantic_core.core_schema.chain_schema:
from pydantic_core import SchemaValidator, core_schema
def add_suffix(v: str) -> str:
return f"{v}_processed"
# Define the steps in the chain
schema = core_schema.chain_schema(
steps=[
core_schema.str_schema(), # Step 1: Ensure input is a string
core_schema.no_info_plain_validator_function(add_suffix), # Step 2: Transform
]
)
v = SchemaValidator(schema)
result = v.validate_python("data")
print(result)
# Output: 'data_processed'
In this example, chain_schema takes a list of schemas. If the first step (string validation) fails, the chain stops. If it succeeds, the resulting string is passed to the add_suffix function.
Step 2: Using the Fluent Pipeline API
While chain_schema is powerful, it can become verbose for complex sequences. The pydantic.experimental.pipeline module provides a fluent interface that compiles down to a ChainSchema.
Build a pipeline that strips whitespace from a string and then validates it as an integer:
from typing import Annotated
from pydantic import TypeAdapter
from pydantic.experimental.pipeline import validate_as
# Define a pipeline: validate as str -> strip -> validate as int
pipeline = validate_as(str).str_strip().validate_as(int)
# Use TypeAdapter to run the pipeline
ta = TypeAdapter[int](Annotated[int, pipeline])
print(ta.validate_python(" 123 "))
# Output: 123
The validate_as(str) call initializes the pipeline. The .str_strip() method adds a transformation step, and the final .validate_as(int) ensures the resulting stripped string can be parsed into an integer.
Step 3: Creating a Complex Currency Processor
Now, let's combine these concepts to build a robust currency processor. This pipeline will:
- Accept a string.
- Strip whitespace.
- Convert the string to a
Decimal. - Ensure the value is greater than zero.
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, Field, ValidationError
from pydantic.experimental.pipeline import validate_as
# Define the reusable currency pipeline
currency_pipeline = (
validate_as(str)
.str_strip()
.validate_as(Decimal)
.gt(Decimal("0"))
)
class Product(BaseModel):
name: str
# Apply the pipeline using Annotated
price: Annotated[Decimal, currency_pipeline]
# Test with valid input
p = Product(name="Laptop", price=" 999.99 ")
print(f"{p.name} costs {p.price} ({type(p.price)})")
# Output: Laptop costs 999.99 (<class 'decimal.Decimal'>)
# Test with invalid input (negative value)
try:
Product(name="Cheap Item", price="-10.00")
except ValidationError as e:
print(e)
# Output: Input should be greater than 0
How it Works
validate_as(str): Ensures the initial input is a string..str_strip(): Internally adds ano_info_plain_validator_functionto theChainSchemathat calls.strip()on the input..validate_as(Decimal): Adds a step that uses Pydantic's internalDecimalvalidator. Because the previous step output a string, this validator parses that string into aDecimalobject..gt(Decimal("0")): Adds a constraint step (usingannotated_types.Gt) that is checked against theDecimalobject.
Important Considerations
- Step Order: The order of steps in
ChainSchemais critical. If you placed.gt()before.validate_as(Decimal), the validation would fail because you cannot perform a "greater than" comparison on a raw string against aDecimal. - Empty Chains: A
ChainSchemamust have at least one step. Providing an empty list tochain_schemawill result in aSchemaError. - Experimental Status: The
pydantic.experimental.pipelineAPI is subject to change. For production-critical code where stability is paramount, consider using the lower-levelcore_schema.chain_schemaor standardAfterValidatorandBeforeValidatorpatterns.
Next Steps
To further customize your pipelines, you can use the transform() function to inject arbitrary Python logic between validation steps, or use the | operator to create unions (branching logic) within your pipeline.