Many AI systems return structured data β Pydantic models, JSON objects, or
nested dicts β rather than plain strings. This guide shows how to validate
individual fields, assert types, and check nested values using Equals,
FnCheck, and JSONPath extraction.
To get started, weβll define the extraction function that all subsequent tests
will target. Returning a Pydantic model rather than a raw dict gives you typed
field access in your check lambdas and makes the test code much easier to read.
Weβll use a simple information-extraction function that returns a Pydantic
model:
from pydantic import BaseModel
classPersonInfo(BaseModel):
name:str
age:int
email:str
occupation:str
defextract_info(text:str)-> PersonInfo:
# Your extraction system (LLM, regex, etc.)
returnPersonInfo(
name="Maria Lopez",
age=52,
email="maria.lopez@acmebank.com",
occupation="Chief Risk Officer",
)
The same pattern applies to any callable that returns a dict, dataclass, or
Pydantic model.
With the extraction function defined, we can now write our first assertion.
Equals is the right choice here because we have a ground-truth value we expect
the model to reproduce exactly β no fuzzy matching needed.
Use Equals with a key path to assert a specific field:
import asyncio
from giskard.checks import Scenario, Equals
tc =(
Scenario("extract_name")
.interact(
inputs=(
"Maria Lopez, 52, Chief Risk Officer at ACME Bank. "
Next, weβll verify fields where the correct value isnβt a single fixed string.
FnCheck lets you express any boolean predicate, so you can validate format
constraints like email structure or numeric bounds without hard-coding the exact
output.
When you need more than equality β a range, a regex, a format check β use
FnCheck:
from giskard.checks import FnCheck
tc =(
Scenario("extract_email")
.interact(
inputs=(
"Maria Lopez, 52, Chief Risk Officer at ACME Bank. "
When your output contains objects nested several levels deep β or lists β dot
notation alone can be ambiguous. The resolve helper traverses both attribute
access and dict-style access uniformly, and returns a NoMatch sentinel instead
of raising an exception when a path doesnβt exist.
For deeply nested data, use the resolve helper from
giskard.checks.core.extraction:
from pydantic import BaseModel
from giskard.checks import Scenario, FnCheck
classAddress(BaseModel):
street:str
city:str
country:str
classContact(BaseModel):
name:str
address: Address
tags:list[str]
defextract_contact(text:str)-> Contact:
returnContact(
name="Jane Smith",
address=Address(street="123 Main St",city="London",country="UK"),
tags=["vip","enterprise"],
)
tc =(
Scenario("nested_extraction")
.interact(
inputs="Jane Smith, 123 Main St, London, UK. Tags: VIP, Enterprise.",
Building on the predicate pattern, we can now apply it to classification tasks
where the output carries both a categorical label and a numeric confidence.
Combining Equals for the label with a threshold check for confidence gives you
a complete quality gate in a single scenario.
For classification tasks, validate both the predicted label and the confidence
score:
from pydantic import BaseModel
from giskard.checks import Scenario, Equals, FnCheck
Now weβll bring all the individual checks together into a suite class that runs
them concurrently. Notice that each scenario constructs its own Scenario at
init time with the extractor injected β this makes the suite easy to reuse
against a different extraction function without changing any test logic.
Group multiple extraction checks into a suite for concurrent execution:
import asyncio
from giskard.checks import Scenario, Equals, FnCheck
classExtractionTestSuite:
def__init__(self,extractor):
self.name_check =(
Scenario("name_extraction")
.interact(
inputs=(
"Maria Lopez, 52, Chief Risk Officer at ACME Bank. "
"Email: maria.lopez@acmebank.com"
),
outputs=lambdainputs:extractor(inputs),
)
.check(
Equals(
name="correct_name",
expected_value="Maria Lopez",
key="trace.last.outputs.name",
)
)
)
self.email_check =(
Scenario("email_extraction")
.interact(
inputs=(
"Maria Lopez, 52, Chief Risk Officer at ACME Bank. "
"Email: maria.lopez@acmebank.com"
),
outputs=lambdainputs:extractor(inputs),
)
.check(
FnCheck(fn=
lambdatrace:"@"in trace.last.outputs.email,
name="valid_email",
)
)
)
self.age_check =(
Scenario("age_extraction")
.interact(
inputs=(
"Maria Lopez, 52, Chief Risk Officer at ACME Bank. "