Stickler: Structured Object Evaluation for GenAI

May 20, 2026 · View on GitHub

Ask DeepWiki

When in the course of human events, it becomes necessary to evaluate structured outputs from generative AI systems, we must acknowledge that traditional evaluation treats all fields equally. But not all fields are created equal.

Stickler is a Python library that enables complex structured JSON comparison and evaluation that lets you focus on the fields your customer actually cares about, to answer the question: "Is it doing a good job?"

Stickler uses specialized comparators for different data types: exact matching for critical identifiers, numeric tolerance for currency amounts, semantic similarity for text fields, and fuzzy matching for names and addresses. You can build custom comparators for domain-specific logic. The Hungarian algorithm ensures optimal list matching regardless of order, while the recursive evaluation engine handles unlimited nesting depth. Business-weighted scoring reflects actual operational impact, not just technical accuracy.

Consider an invoice extraction agent that perfectly captures shipment numbers (which must be exact or packages get routed to the wrong warehouse) but sometimes garbles driver notes like "delivered to front door" vs "left at entrance." Those note variations don't affect logistics operations at all. Traditional evaluation treats both error types identically and reports your agent as "95% accurate" without telling you if that 5% error rate matters. Stickler tells you exactly where the errors are and whether they're actually problems.

Whether you're extracting data from documents, performing ETL transformations, evaluating ML model outputs, or simply trying to diff complex JSON structures, Stickler transforms evaluation from a technical afterthought into a business-aligned decision tool.

Installation

pip install stickler-eval

Get Started in 30 Seconds

# pip install stickler-eval
from typing import List
from stickler import StructuredModel, ComparableField
from stickler.comparators import ExactComparator, NumericComparator, LevenshteinComparator

# Define your models
class LineItem(StructuredModel):
    product: str = ComparableField(comparator=LevenshteinComparator(), weight=1.0)
    quantity: int = ComparableField(weight=0.8)
    price: float = ComparableField(comparator=NumericComparator(tolerance=0.01), weight=1.2)

class Invoice(StructuredModel):
    shipment_id: str = ComparableField(comparator=ExactComparator(), weight=3.0)  # Critical
    amount: float = ComparableField(comparator=NumericComparator(tolerance=0.01), weight=2.0)
    line_items: List[LineItem] = ComparableField(weight=2.0)  # Hungarian matching!

# JSON from your systems (agent output, ground truth, etc.)
ground_truth_json = {
    "shipment_id": "SHP-2024-001",
    "amount": 1247.50,
    "line_items": [
        {"product": "Wireless Mouse", "quantity": 2, "price": 29.99},
        {"product": "USB Cable", "quantity": 5, "price": 12.99}
    ]
}

prediction_json = {
    "shipment_id": "SHP-2024-001",  # Perfect match
    "amount": 1247.48,  # Within tolerance
    "line_items": [
        {"product": "USB Cord", "quantity": 5, "price": 12.99},  # Name variation
        {"product": "Wireless Mouse", "quantity": 2, "price": 29.99}  # Reordered
    ]
}

# Construct from JSON and compare
ground_truth = Invoice(**ground_truth_json)
prediction = Invoice(**prediction_json)
result = ground_truth.compare_with(prediction)

print(f"Overall Score: {result['overall_score']:.3f}")  # 0.693
print(f"Shipment ID: {result['field_scores']['shipment_id']:.3f}")  # 1.000 - exact match
print(f"Line Items: {result['field_scores']['line_items']:.3f}")  # 0.926 - Hungarian optimal matching

Requirements

  • Python 3.12+
  • uv (recommended)

Quick Install

# Install from PyPI
pip install stickler-eval

# Or clone and install for development (uv handles Python + venv automatically)
git clone https://github.com/awslabs/stickler.git
cd stickler
uv sync

Quick Test

Run the example to verify installation:

python examples/scripts/quick_start.py

Run tests:

uv run pytest tests/
# or, if using pip + venv:
pytest tests/

Basic Usage

Static Model Definition

from stickler import StructuredModel, ComparableField
from stickler.comparators.levenshtein import LevenshteinComparator

# Define your data structure
class Invoice(StructuredModel):
    invoice_number: str = ComparableField(
        comparator=LevenshteinComparator(),
        threshold=0.9
    )
    total: float = ComparableField(threshold=0.95)

# Compare objects
result = ground_truth.compare_with(prediction, evaluator_format=True)

print(f"Overall Score: {result['overall']['anls_score']:.3f}")

Confidence Evaluation

Evaluate prediction confidence calibration with pluggable metrics:

# Prediction with confidence scores
prediction = Invoice.from_json({
    "invoice_number": {"_value": "INV-2024-001", "_confidence": 0.95},
    "total": {"_value": 1247.50, "_confidence": 0.8}
})

# Single-document confidence metrics
result = ground_truth.compare_with(
    prediction,
    add_confidence_metrics=True,
    document_field_comparisons=True
)
print(result["confidence_metrics"]["overall"])   # {"auroc": {"value": ...}}
# Both fields came in with _confidence, so coverage is 2/2:
print(result["confidence_metrics"]["coverage"])  # {"fields_with_confidence": 2, "fields_total": 2, "ratio": 1.0}

# Bulk evaluation (recommended, dataset-level AUROC is statistically meaningful)
from stickler.structured_object_evaluator.bulk_structured_model_evaluator import BulkStructuredModelEvaluator
from stickler.structured_object_evaluator.models.confidence import AUROCMetric, BrierScoreMetric, ECEMetric

evaluator = BulkStructuredModelEvaluator(
    target_schema=Invoice,
    confidence_metrics=[AUROCMetric(), BrierScoreMetric(), ECEMetric(n_bins=10)]
)
for gt, pred in dataset:
    evaluator.update(gt, pred)
results = evaluator.compute()
print(results.confidence_metrics["overall"]["auroc"]["value"])

Dynamic Model Creation (New!)

Create models from JSON configuration for maximum flexibility:

from stickler.structured_object_evaluator.models.structured_model import StructuredModel

# Define model configuration
config = {
    "model_name": "Product",
    "match_threshold": 0.8,
    "fields": {
        "name": {
            "type": "str",
            "comparator": "LevenshteinComparator",
            "threshold": 0.8,
            "weight": 2.0
        },
        "price": {
            "type": "float",
            "comparator": "NumericComparator",
            "default": 0.0
        }
    }
}

# Create dynamic model class
Product = StructuredModel.model_from_json(config)

# Use like any Pydantic model
product1 = Product(name="Widget", price=29.99)
product2 = Product(name="Gadget", price=29.99)

# Full comparison capabilities
result = product1.compare_with(product2)
print(f"Similarity: {result['overall_score']:.2f}")

Complete JSON-to-Evaluation Workflow (New!)

For maximum flexibility, load both configuration AND data from JSON:

# Load model config from JSON
with open('model_config.json') as f:
    config = json.load(f)

# Load test data from JSON  
with open('test_data.json') as f:
    data = json.load(f)

# Create model and instances from JSON
Model = StructuredModel.model_from_json(config)
ground_truth = Model(**data['ground_truth'])
prediction = Model(**data['prediction'])

# Evaluate - no Python object construction needed!
result = ground_truth.compare_with(prediction)

Benefits of JSON-Driven Approach:

  • Zero Python object construction required
  • Configuration-driven model creation
  • A/B testing different field configurations
  • Runtime model generation from external schemas
  • Production-ready JSON-based evaluation pipeline
  • Full Pydantic compatibility with comparison capabilities

See examples/scripts/json_to_evaluation_demo.py for a complete working example and docs/StructuredModel_Dynamic_Creation.md for comprehensive documentation.

JSON Schema Extensions: x-aws-stickler-* Complete Reference

Stickler supports standard JSON Schema (Draft 7+) with custom x-aws-stickler-* extensions for controlling comparison behavior. These extensions let you configure exactly how each field is evaluated without writing Python code.

Why Use JSON Schema Extensions?

  • Configuration-driven evaluation: Define models and comparison logic in JSON
  • No Python code required: Perfect for non-Python systems or runtime configuration
  • Version control friendly: Track evaluation logic changes alongside your schemas
  • A/B testing: Easily test different comparison strategies
  • Integration ready: Works with existing JSON Schema tooling and validators

Field-Level Extensions

Add these to any property in your JSON Schema to control comparison behavior:

x-aws-stickler-comparator

Type: string
Required: No
Default: Type-dependent (see table below)

Specifies the comparison algorithm for this field.

Available Comparators:

Comparator NameBest ForHow It Works
"LevenshteinComparator"Names, addresses, text with typosCalculates edit distance between strings. Score = 1 - (edits / max_length)
"ExactComparator"IDs, codes, booleans, exact matchesReturns 1.0 for exact match, 0.0 otherwise
"NumericComparator"Prices, quantities, measurementsCompares numbers with configurable tolerance
"FuzzyComparator"Flexible text, descriptionsToken-based fuzzy matching (order-independent)
"SemanticComparator"Semantic similarityEmbedding-based comparison for meaning
"BertComparator"Deep semantic understandingBERT model for contextual similarity
"LLMComparator"Complex semantic evaluationLLM-powered comparison with reasoning

Default Comparators by JSON Schema Type:

JSON Schema TypeDefault ComparatorDefault ThresholdRationale
"string"LevenshteinComparator0.5Handles typos and minor variations
"number"NumericComparator0.5Tolerates small numeric differences
"integer"NumericComparator0.5Tolerates small numeric differences
"boolean"ExactComparator1.0Must be exactly true or false
"array" (primitives)Based on item typeBased on item typeInherits from element type
"array" (objects)Hungarian matching0.7Optimal pairing of list elements
"object"Recursive comparison0.7Field-by-field nested comparison

Example:

{
  "type": "object",
  "properties": {
    "invoice_id": {
      "type": "string",
      "description": "Must match exactly - routing depends on this",
      "x-aws-stickler-comparator": "ExactComparator"
    },
    "customer_name": {
      "type": "string",
      "description": "Allow typos but catch major errors",
      "x-aws-stickler-comparator": "LevenshteinComparator"
    },
    "description": {
      "type": "string",
      "description": "Flexible matching for free-form text",
      "x-aws-stickler-comparator": "FuzzyComparator"
    }
  }
}

x-aws-stickler-threshold

Type: number (0.0 to 1.0, inclusive)
Required: No
Default: 0.5 (or 1.0 for booleans)

Minimum similarity score required for binary match classification.

How Threshold Works:

  • Similarity score ≥ threshold → Classified as MATCH (True Positive or True Negative)
  • Similarity score < threshold → Classified as NO MATCH (False Positive or False Negative)

Threshold Selection Guide:

Threshold RangeUse CaseExample Fields
1.0Must be perfectIDs, codes, critical identifiers
0.9 - 0.95Very important, minimal toleranceAmounts, dates, status codes
0.8 - 0.85Important, some toleranceNames, addresses, product codes
0.7 - 0.75Moderate toleranceDescriptions, categories
0.5 - 0.6Flexible, variations OKNotes, comments, metadata

Example:

{
  "type": "object",
  "properties": {
    "shipment_id": {
      "type": "string",
      "description": "CRITICAL: Wrong ID = wrong warehouse",
      "x-aws-stickler-comparator": "ExactComparator",
      "x-aws-stickler-threshold": 1.0
    },
    "total_amount": {
      "type": "number",
      "description": "Very important: affects billing",
      "x-aws-stickler-comparator": "NumericComparator",
      "x-aws-stickler-threshold": 0.95
    },
    "customer_name": {
      "type": "string",
      "description": "Important: allow minor typos",
      "x-aws-stickler-comparator": "LevenshteinComparator",
      "x-aws-stickler-threshold": 0.8
    },
    "driver_notes": {
      "type": "string",
      "description": "Flexible: variations don't affect operations",
      "x-aws-stickler-comparator": "FuzzyComparator",
      "x-aws-stickler-threshold": 0.6
    }
  }
}

x-aws-stickler-weight

Type: number (positive float, > 0.0)
Required: No
Default: 1.0

Relative importance of this field in aggregate scoring and weighted averages.

How Weights Work:

  • Fields with weight > 1.0 have greater influence on overall scores
  • Fields with weight < 1.0 have less influence on overall scores
  • Used in weighted average calculations: overall_score = Σ(field_score × weight) / Σ(weight)

Weight Selection Guide:

Weight RangePriority LevelUse Case
2.5 - 3.0CriticalBusiness-critical fields (IDs, amounts, status)
1.5 - 2.0HighImportant operational fields
1.0NormalStandard fields
0.5 - 0.8LowNice-to-have fields
0.1 - 0.3MinimalMetadata, debug fields

Example:

{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string",
      "description": "CRITICAL: Wrong order = wrong customer",
      "x-aws-stickler-comparator": "ExactComparator",
      "x-aws-stickler-threshold": 1.0,
      "x-aws-stickler-weight": 3.0
    },
    "total_amount": {
      "type": "number",
      "description": "Very important: affects billing",
      "x-aws-stickler-comparator": "NumericComparator",
      "x-aws-stickler-threshold": 0.95,
      "x-aws-stickler-weight": 2.5
    },
    "customer_name": {
      "type": "string",
      "description": "Important but not critical",
      "x-aws-stickler-comparator": "LevenshteinComparator",
      "x-aws-stickler-threshold": 0.8,
      "x-aws-stickler-weight": 1.0
    },
    "internal_notes": {
      "type": "string",
      "description": "Low priority: internal use only",
      "x-aws-stickler-comparator": "FuzzyComparator",
      "x-aws-stickler-threshold": 0.6,
      "x-aws-stickler-weight": 0.3
    }
  }
}

x-aws-stickler-clip-under-threshold

Type: boolean
Required: No
Default: false

Controls whether similarity scores below threshold are clipped to 0.0.

How Clipping Works:

  • true: Scores below threshold are set to 0.0 (hard cutoff)
  • false: Actual similarity scores are preserved (soft cutoff)

Important: This affects continuous similarity metrics but NOT binary classification (TP/FP/TN/FN). Binary classification always uses threshold as a hard cutoff.

When to Use:

SettingUse CaseEffect
trueCritical fields where partial matches are meaninglessScore is either threshold or 0.0
falseFields where partial similarity has valuePreserves granular similarity information

Example:

{
  "type": "object",
  "properties": {
    "status_code": {
      "type": "string",
      "description": "Either correct or completely wrong",
      "x-aws-stickler-comparator": "ExactComparator",
      "x-aws-stickler-threshold": 1.0,
      "x-aws-stickler-clip-under-threshold": true
    },
    "description": {
      "type": "string",
      "description": "Partial matches still have value",
      "x-aws-stickler-comparator": "LevenshteinComparator",
      "x-aws-stickler-threshold": 0.8,
      "x-aws-stickler-clip-under-threshold": false
    }
  }
}

x-aws-stickler-aggregate

Type: boolean
Required: No
Default: false

Controls whether this field's confusion matrix metrics (TP/FP/TN/FN) are included in parent-level aggregate counts.

How Aggregation Works:

  • true: Field's metrics are included in parent's aggregate counts
  • false: Field's metrics are calculated but not aggregated to parent

When to Use:

SettingUse Case
trueInclude field in overall accuracy/precision/recall calculations
falseExclude field from aggregate metrics (debugging, metadata, experimental fields)

Example:

{
  "type": "object",
  "properties": {
    "invoice_id": {
      "type": "string",
      "description": "Include in accuracy metrics",
      "x-aws-stickler-comparator": "ExactComparator",
      "x-aws-stickler-threshold": 1.0,
      "x-aws-stickler-aggregate": true
    },
    "debug_field": {
      "type": "string",
      "description": "For debugging only - don't affect metrics",
      "x-aws-stickler-comparator": "ExactComparator",
      "x-aws-stickler-threshold": 1.0,
      "x-aws-stickler-aggregate": false
    }
  }
}

Model-Level Extensions

Add these at the root level of your JSON Schema:

x-aws-stickler-model-name

Type: string
Required: No
Default: "DynamicModel"

Name of the generated Python class.

{
  "type": "object",
  "x-aws-stickler-model-name": "Invoice",
  "properties": { ... }
}

x-aws-stickler-match-threshold

Type: number (0.0 to 1.0)
Required: No
Default: 0.7

Overall matching threshold for the model. Used for Hungarian algorithm matching when comparing lists of objects.

{
  "type": "object",
  "x-aws-stickler-model-name": "LineItem",
  "x-aws-stickler-match-threshold": 0.8,
  "properties": { ... }
}

Complete Real-World Example

Here's a production-ready invoice schema with all extensions:

{
  "type": "object",
  "x-aws-stickler-model-name": "Invoice",
  "x-aws-stickler-match-threshold": 0.75,
  "description": "Invoice extraction schema with business-aligned comparison",
  "properties": {
    "invoice_id": {
      "type": "string",
      "description": "Unique invoice identifier - must be exact",
      "examples": ["INV-2024-001", "INV-2024-002"],
      "x-aws-stickler-comparator": "ExactComparator",
      "x-aws-stickler-threshold": 1.0,
      "x-aws-stickler-weight": 3.0,
      "x-aws-stickler-clip-under-threshold": true,
      "x-aws-stickler-aggregate": true
    },
    "customer_name": {
      "type": "string",
      "description": "Customer's full name - allow minor typos",
      "examples": ["John Smith", "Acme Corporation"],
      "x-aws-stickler-comparator": "LevenshteinComparator",
      "x-aws-stickler-threshold": 0.8,
      "x-aws-stickler-weight": 1.5,
      "x-aws-stickler-clip-under-threshold": false,
      "x-aws-stickler-aggregate": true
    },
    "total_amount": {
      "type": "number",
      "description": "Total invoice amount in USD - critical for billing",
      "examples": [1234.56, 99.99],
      "x-aws-stickler-comparator": "NumericComparator",
      "x-aws-stickler-threshold": 0.95,
      "x-aws-stickler-weight": 2.5,
      "x-aws-stickler-clip-under-threshold": false,
      "x-aws-stickler-aggregate": true
    },
    "line_items": {
      "type": "array",
      "description": "Individual line items - uses Hungarian matching",
      "items": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "description": "Item description",
            "x-aws-stickler-comparator": "FuzzyComparator",
            "x-aws-stickler-threshold": 0.7,
            "x-aws-stickler-weight": 1.0
          },
          "quantity": {
            "type": "integer",
            "description": "Item quantity",
            "x-aws-stickler-comparator": "NumericComparator",
            "x-aws-stickler-threshold": 1.0,
            "x-aws-stickler-weight": 1.2
          },
          "unit_price": {
            "type": "number",
            "description": "Price per unit",
            "x-aws-stickler-comparator": "NumericComparator",
            "x-aws-stickler-threshold": 0.95,
            "x-aws-stickler-weight": 1.5
          }
        },
        "required": ["description", "quantity", "unit_price"]
      }
    },
    "internal_notes": {
      "type": "string",
      "description": "Internal processing notes - low priority",
      "x-aws-stickler-comparator": "FuzzyComparator",
      "x-aws-stickler-threshold": 0.5,
      "x-aws-stickler-weight": 0.2,
      "x-aws-stickler-clip-under-threshold": false,
      "x-aws-stickler-aggregate": false
    }
  },
  "required": ["invoice_id", "customer_name", "total_amount", "line_items"]
}

Using Your Schema

from stickler import StructuredModel
import json

# Load schema
with open('invoice_schema.json') as f:
    schema = json.load(f)

# Create model class
Invoice = StructuredModel.from_json_schema(schema)

# Load test data
ground_truth = Invoice(**{
    "invoice_id": "INV-2024-001",
    "customer_name": "Acme Corporation",
    "total_amount": 1250.00,
    "line_items": [
        {"description": "Widget A", "quantity": 10, "unit_price": 50.00},
        {"description": "Widget B", "quantity": 5, "unit_price": 100.00}
    ],
    "internal_notes": "Processed by system A"
})

prediction = Invoice(**{
    "invoice_id": "INV-2024-001",
    "customer_name": "ACME Corp",  # Variation
    "total_amount": 1250.00,
    "line_items": [
        {"description": "Widget B", "quantity": 5, "unit_price": 100.00},  # Reordered
        {"description": "Widget A", "quantity": 10, "unit_price": 50.00}   # Reordered
    ],
    "internal_notes": "Processed by system B"  # Different
})

# Compare
result = ground_truth.compare_with(prediction)

print(f"Overall Score: {result['overall_score']:.3f}")
print(f"Invoice ID: {result['field_scores']['invoice_id']:.3f}")  # 1.000 - exact
print(f"Customer: {result['field_scores']['customer_name']:.3f}")  # ~0.85 - close
print(f"Line Items: {result['field_scores']['line_items']:.3f}")  # ~1.0 - matched

Quick Reference

ExtensionTypeDefaultPurpose
x-aws-stickler-comparatorstringType-dependentComparison algorithm
x-aws-stickler-thresholdnumber (0.0-1.0)0.5 or 1.0Match classification cutoff
x-aws-stickler-weightnumber (> 0.0)1.0Field importance multiplier
x-aws-stickler-clip-under-thresholdbooleanfalseZero out low scores
x-aws-stickler-aggregatebooleanfalseInclude in parent metrics
x-aws-stickler-model-namestring"DynamicModel"Generated class name
x-aws-stickler-match-thresholdnumber (0.0-1.0)0.7Model-level threshold

Additional Resources

Examples

Check out the examples/ directory for more detailed usage examples and notebooks.

A note for AI assisted coding agents

  • The project uses coding assistant agnostic context files, like README.md and AGENTS.md
  • When working in a directory, always look for a README.md and/or AGENTS.md file for important context about the directory/code contained within.
  • Read this AGENTS.md file for more information