Skip to content

Choosing a Configuration Path

Stickler has two ways to tell it how to compare your fields, and they are independent. They take different inputs, infer different comparators, and score the same data differently. Neither accepts the other's input, so this is a choice you make once per project.

Inference JSON Schema
Entry points stickler.evaluate, stickler.eval_for, StructuredModel.from_pydantic StructuredModel.from_json_schema
Input a live Pydantic BaseModel class a schema dict
Use it when you already have a model — an agent's response_model, an API contract your config lives outside Python, or is edited without a deploy

What each one infers

Inference JSON Schema
From the type yes — bool/Enum → Exact, int → Numeric(exact), float → Numeric(rel_tol 0.001), date → Date, str → Levenshtein yes, coarser — the property is parsed to a Python annotation and the comparator comes from that, so string → Levenshtein, number/integer → Numeric, boolean → Exact, and any annotation outside that set → Exact @ 1.0
From the field name yes — *_id → Exact(case-sensitive), notes → Fuzzy(token_set), total → Numeric(rel_tol), phone → Phone no
"format": "date" n/a — a date annotation is already Date honoured — parses as date, so the field gets Date @ 1.0. "enum", "const", "uri", "uuid", "time" likewise get Exact @ 1.0; a format with no distinct type ("email", "hostname", "duration") stays Levenshtein @ 0.5
Default threshold per comparator, 0.6–1.0 0.5 for the four primitives, 1.0 for anything parsed from a format, enum or const
Introspection .explain(), with a provenance trail per field none; round-trip the class through to_json_schema() to read back what it resolved to
Takes the other's input no — eval_for({...}) raises TypeError no — from_json_schema(MyModel) raises ValueError

Weights are 1.0 on both paths. Inference never picks SemanticComparator, BERTComparator, or LLMComparator — those need models or credentials, so they are explicit-only.

The schema path keeps no separate record of what it chose, so read it back off the exported schema. Note that the parsed annotation is widened to the JSON value type after the comparator is picked, so model_fields reports Optional[str] even for the format/enum rows above — to_json_schema() is the accurate view:

s = {"type": "object", "properties": {
    "d": {"type": "string", "format": "date"},
    "e": {"type": "string", "enum": ["ALPHA", "BETA"]},
    "plain": {"type": "string"}}}
props = StructuredModel.from_json_schema(s).to_json_schema()["properties"]
{k: (v["x-aws-stickler-comparator"], v["x-aws-stickler-threshold"]) for k, v in props.items()}
# {'d':     ('DateComparator',        1.0),
#  'e':     ('ExactComparator',       1.0),
#  'plain': ('LevenshteinComparator', 0.5)}

The divergence is not academic

Six fields, one dataset, both paths. Same names, same types, no x-aws-stickler-* extensions:

class Invoice(BaseModel):
    invoice_id: str
    customer_name: str
    notes: Optional[str] = None
    issue_date: date
    total: float
    quantity: int
Field Inference JSON Schema
invoice_id ExactComparator @ 1.0 LevenshteinComparator @ 0.5
customer_name LevenshteinComparator @ 0.85 LevenshteinComparator @ 0.5
notes FuzzyComparator @ 0.6 LevenshteinComparator @ 0.5
issue_date DateComparator @ 0.95 DateComparator @ 1.0
total NumericComparator @ 0.95 NumericComparator @ 0.5
quantity NumericComparator @ 1.0 NumericComparator @ 0.5

Two of six comparators and six of six thresholds differ. The schema here is Invoice.model_json_schema(), so issue_date carries "format": "date" and both paths reach DateComparator — hand-write the property as a bare {"type": "string"} and the schema path drops to Levenshtein instead. That one omission is worth a whole field: on "2024-01-05" against "Jan 5, 2024", the "format": "date" property scores 1.0 where the bare {"type": "string"} scores 0.0. The scored pair below holds issue_date identical, so it contributes 1.0 on both paths and is not one of the fields that diverges there. customer_name is the subtle case: the same comparator on both paths, separated only by the threshold. Scoring one realistic prediction ("inv-001" for "INV-001", "Acme Corp." for "Acme Corporation", "friday delivery" for "deliver by friday") gives 0.646 by inference and 0.760 by schema — and the two disagree in opposite directions on three fields:

Field Inference JSON Schema Why
invoice_id 0.000 1.000 Exact is case-sensitive for identifiers; Levenshtein normalizes case first
notes 0.875 0.000 Fuzzy token_set_ratio ignores word order; Levenshtein scores the reorder below 0.5 and clips
customer_name 0.000 0.562 same comparator, but 0.562 falls under inference's 0.85 threshold and is clipped

Neither number is wrong. Inference encodes what the field means — an ID that differs in case may be a different ID, and reordered free text usually is not an error. The schema path reads structure, not meaning: it can tell a date from a string, but not an identifier from a paragraph of notes. Pick the one that matches the question you are asking, and do not compare scores across the two.

Moving between them

StructuredModel.from_pydantic(cls).to_json_schema() exports inference's decisions as a schema — comparators, thresholds, nullable fields as ["string", "null"], and the required list. Use it to start from a Pydantic model and hand the result to a config-driven pipeline, or to see the schema that would reproduce what inference chose.

Next