Examples
Example: invalid response recovery
A complete, runnable example of result validation. A provider returns a successful response with unusable data, RelPrim rejects it and retries, and the report records why.
A successful call is not the same as a usable result. This example uses validation to treat a well-formed but unusable response as a failed attempt, so the normal retry policy can recover from it.
The problem
A checkout page asks a pricing service for a quote. The service answers with HTTP 200, but sometimes the body has no price. Nothing raises an exception, so without validation the bad quote flows straight into the order. Checking it by hand after the call means retry logic has to be written again for this one failure.
Implementation
The validator is a plain function, so the example has no dependencies. The demo provider returns a quote without a price on its first call.
import asyncio
from relprim import (
RetryPolicy,
ValidationFailedError,
async_operation,
validation_policy,
validator,
)
class DemoQuoteProvider:
"""Answers with HTTP 200 every time, but the first body is unusable."""
def __init__(self) -> None:
self.calls = 0
async def quote(self, sku: str) -> dict[str, object]:
self.calls += 1
if self.calls == 1:
return {"sku": sku, "price_cents": None}
return {"sku": sku, "price_cents": 1299}
def has_positive_price(quote: dict[str, object]) -> bool:
price = quote.get("price_cents")
return isinstance(price, int) and price > 0
async def no_sleep(delay: float) -> None:
"""Skip the backoff delay so the example runs instantly."""
async def main() -> None:
provider = DemoQuoteProvider()
result = await (
async_operation("fetch_quote", provider.quote)
.with_validation(
validation_policy(
validator(
"positive_price",
has_positive_price,
message="Quote must contain a positive price_cents.",
)
)
)
.with_retry(
RetryPolicy(
max_attempts=3,
retry_on=(ValidationFailedError,),
async_sleeper=no_sleep,
)
)
.run("SKU-42")
)
report = result.report
print(result.value)
print(f"attempt_count={report.attempt_count} retry_count={report.retry_count}")
first_error = report.attempts[0].error
if first_error is not None:
print(f"attempt 1: {first_error.type}: {first_error.message}")
print(f"final validation valid: {report.metadata['validation_valid']}")
asyncio.run(main())
It prints:
{'sku': 'SKU-42', 'price_cents': 1299}
attempt_count=2 retry_count=1
attempt 1: ValidationFailedError: Validation failed in 'positive_price': Quote must contain a positive price_cents.
final validation valid: True
What happens
- Attempt 1 returns
{"price_cents": None}. The call itself succeeded, but thepositive_pricevalidator returnsFalse, so the attempt fails withValidationFailedError. Its message names the validator and the reason. ValidationFailedErrormatchesretry_on, so the attempt is retried. The defaultretry_on=(Exception,)would match it too; once you narrowretry_on, list it explicitly as here.- Attempt 2 returns a priced quote. It passes validation and becomes
result.value.
Validation runs after each attempt, before the retry decision, and the value is
returned unchanged: a validator accepts or rejects, it does not transform. The
report metadata describes the final validation: validation_performed,
validation_valid, validation_validator_name and validation_reason.
Why this matters
“Transport success” and “valid business result” become two separate checks with one recovery path. The code that uses the quote never sees the invalid one, and the report says which validator rejected which attempt.
Production note
Only retry a validation failure when a new attempt can plausibly return
something different, as with flaky upstream data or generated content. A
validation failure caused by a bad request will fail every time; leave
ValidationFailedError out of retry_on there, or add a
fallback, whose candidates are validated the same way.
If the response already has a schema or a model, use it instead of a hand-written check:
- JSON Schema:
json_schema_validator(schema). - Pydantic:
pydantic_validator(Model).
Both plug into validation_policy(...) exactly like the validator above.
Related documentation
- Validation: validators, policies and ordering.
- Retry:
retry_onand backoff. - Runnable repository examples:
validation.py,json_schema_validation.pyandpydantic_validation.py.
