Guardrails AIguardrails-ai 0.11.0 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson

Writing your own validator

Lesson 7 showed what a validator returns. The shop's actual rule, never promise a refund, is not on PyPI and never will be, so this lesson writes it.

python
from typing import Dict

from guardrails import Guard, register_validator
from guardrails.validators import FailResult, PassResult, ValidationResult, Validator

Four imports: the decorator that registers a validator, the base class, and the two results a validator can return.

Example
@register_validator(name="shop/no-refunds", data_type="string")
class NoRefunds(Validator):
    def _validate(self, value: str, metadata: Dict) -> ValidationResult:
        if "refund" in value.lower():
            return FailResult(error_message="The reply promises a refund.")
        return PassResult()


print(NoRefunds.rail_alias)

Two pieces do the work. @register_validator puts the class in the registry under a name, which is what rail_alias reads back and what a Guard stores in its logs. The name is yours to choose; prefixing it with something like shop/ keeps it away from the ones Guardrails ships.

The method is _validate, with the underscore. The base class has a public validate whose own docstring says not to override it, because it is a wrapper that the streaming machinery in lesson 21 hooks into.

Example
desk = Guard().use(NoRefunds(on_fail="noop"))

print(desk.validate("Order 8821 ships today.").validation_passed)
print(desk.validate("We can refund order 8821.").validation_summaries[0].failure_reason)

A rule whose answer changes at run time

The list of competitors is not something to bake into a class. Guardrails passes a dictionary called metadata down to every validator, and you fill it in when you call the Guard rather than when you build it. The Validators page calls this runtime metadata.

Example
@register_validator(name="shop/no-competitors", data_type="string")
class NoCompetitors(Validator):
    def _validate(self, value: str, metadata: Dict) -> ValidationResult:
        named = [c for c in metadata.get("competitors", []) if c.lower() in value.lower()]
        if named:
            return FailResult(error_message=f"The reply names {named[0]}.")
        return PassResult()


print(NoCompetitors.rail_alias)
Example
rivals = Guard().use(NoCompetitors(on_fail="noop"))
facts = {"competitors": ["ShopFast", "QuickCart"]}

print(rivals.validate("Try ShopFast instead.", metadata=facts).validation_summaries[0].failure_reason)
print(rivals.validate("Try ShopFast instead.", metadata={}).validation_passed)

Constructor arguments are settings; metadata is data. The same Guard checked the same sentence twice and disagreed with itself, because the second call was told there were no competitors to worry about.

The short form

A rule that needs no settings can be a function.

Example
@register_validator(name="shop/no-shouting", data_type="string")
def no_shouting(value, metadata: Dict) -> ValidationResult:
    if value.isupper():
        return FailResult(error_message="The reply is in capitals.", fix_value=value.lower())
    return PassResult()


print(type(no_shouting))
print(Guard().use(no_shouting(on_fail="noop")).validate("SORRY").validation_passed)

The decorator does not hand back a function. It builds a class around it, which is why the Guard gets no_shouting(on_fail="noop") with brackets, exactly like ValidLength(min=1, max=20). Forgetting the brackets gives you the error lesson 6 ended on.

Try it yourself
  • Give NoRefunds a fix_value that swaps refund for store credit, then read lesson 10.
  • Put a print inside _validate and run a Guard with two validators. You will see them interleave.
  • Register two validators under the same name and see which one a Guard picks up.

Slow is fine. Stopping is the only problem.