Advanced Use Cases
On this page
Beyond Simple Examples
You have mastered Pydantic, Zod, and client.chat.parse(). The next three cases move beyond simple four-field schemas to scenarios you will encounter in enterprise environments: accounting documents, detailed customer review analysis, and texts that belong to multiple categories simultaneously. Each illustrates a different modeling technique.
Extracting an Invoice
Document processing is the natural territory for nested schemas. An invoice is not a flat object: it contains a header, a footer with totals, and a variable number of line items that have their own structure. Therefore, we model LineItem separately and then reference it within a list. The currency uses an enum, which prevents the model from returning "euros" or "€" depending on how the document is phrased.
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class Currency(str, Enum):
eur = "EUR"
usd = "USD"
gbp = "GBP"
class LineItem(BaseModel):
description: str = Field(description="Line item description")
quantity: float = Field(description="Quantity")
unit_price: float = Field(description="Unit price excluding tax")
vat_rate: float = Field(description="VAT rate as a percentage (e.g., 20.0)")
total_ht: float = Field(description="Line total excluding tax")
class InvoiceData(BaseModel):
invoice_number: str = Field(description="Invoice number")
date: str = Field(description="Invoice date in YYYY-MM-DD format")
due_date: Optional[str] = Field(default=None, description="Due date")
vendor_name: str = Field(description="Vendor name")
vendor_address: Optional[str] = Field(default=None)
client_name: str = Field(description="Client name")
line_items: list[LineItem] = Field(description="Invoice line items")
subtotal_ht: float = Field(description="Subtotal excluding tax")
total_vat: float = Field(description="Total VAT amount")
total_ttc: float = Field(description="Total including tax")
currency: Currency = Field(description="Currency")
payment_method: Optional[str] = Field(default=None)
# Extraction
response = client.chat.parse(
model="mistral-large-latest",
messages=[
{
"role": "system",
"content": """You are an accounting expert. Extract data from the invoice.
Be precise with amounts. Calculate totals if necessary.
Dates must be in YYYY-MM-DD format."""
},
{"role": "user", "content": invoice_text}
],
response_format=InvoiceData,
temperature=0
)
invoice = response.choices[0].message.parsed
print(f"Invoice {invoice.invoice_number} — {invoice.vendor_name}")
print(f"Total incl. tax: {invoice.total_ttc} {invoice.currency.value}")
for item in invoice.line_items:
print(f" - {item.description}: {item.quantity} x {item.unit_price}€ = {item.total_ht}€ excl. tax")
The date format enforced in the system prompt is not a minor detail: without it, a French document might produce 12/03/2026 while an American one yields 03/12/2026, creating two indistinguishable strings for your database. The TypeScript version follows the same architecture, with the format documented via .describe().
import { z } from "zod";
const LineItemSchema = z.object({
description: z.string(),
quantity: z.number(),
unitPrice: z.number().describe("Unit price excluding tax"),
vatRate: z.number().describe("VAT rate in %"),
totalHt: z.number(),
});
const InvoiceSchema = z.object({
invoiceNumber: z.string(),
date: z.string().describe("Format YYYY-MM-DD"),
dueDate: z.string().nullable(),
vendorName: z.string(),
clientName: z.string(),
lineItems: z.array(LineItemSchema),
subtotalHt: z.number(),
totalVat: z.number(),
totalTtc: z.number(),
currency: z.enum(["EUR", "USD", "GBP"]),
});
const response = await client.chat.parse({
model: "mistral-large-latest",
messages: [
{ role: "system", content: "Extract invoice data precisely." },
{ role: "user", content: invoiceText },
],
responseFormat: InvoiceSchema,
temperature: 0,
});
const invoice = response.choices[0].message.parsed!;
console.log(`Invoice ${invoice.invoiceNumber} — Total: ${invoice.totalTtc} ${invoice.currency}`);
Analyzing Reviews Aspect by Aspect
A real customer review is rarely uniformly positive or negative. The hotel review below praises the room and breakfast, condemns the parking and Wi-Fi, and offers a nuanced view of the staff: a single global score would lose all this information. The solution is to model a reusable AspectSentiment and request a list of them. The evidence field is the most interesting part of the schema—by requiring a quote from the text, you force the model to ground its judgment and give yourself the means to verify it.
from pydantic import BaseModel, Field
class AspectSentiment(BaseModel):
aspect: str = Field(description="The aspect analyzed (quality, price, service, delivery, etc.)")
sentiment: str = Field(description="positive, negative, or neutral")
score: float = Field(description="Score between -1.0 (very negative) and 1.0 (very positive)")
evidence: str = Field(description="Quote or excerpt from the text justifying the sentiment")
class ReviewAnalysis(BaseModel):
overall_sentiment: str = Field(description="Overall sentiment: positive, negative, mixed, or neutral")
overall_score: float = Field(description="Overall score between -1.0 and 1.0")
aspects: list[AspectSentiment] = Field(description="Aspect-by-aspect analysis")
recommendation: bool = Field(description="Does the author recommend the product/service?")
key_strengths: list[str] = Field(description="Identified strengths")
key_weaknesses: list[str] = Field(description="Identified weaknesses")
review_text = """
The hotel is magnificent; the room was spacious and clean. The breakfast is excellent
with a wide selection. However, the parking is outrageously expensive (€35/night) and the Wi-Fi was
unstable throughout the stay. The reception staff was very welcoming,
but room service took over an hour to deliver our order.
"""
response = client.chat.parse(
model="mistral-large-latest",
messages=[
{
"role": "system",
"content": "Analyze this customer review by detailing each mentioned aspect."
},
{"role": "user", "content": review_text}
],
response_format=ReviewAnalysis,
temperature=0
)
review = response.choices[0].message.parsed
print(f"Overall sentiment: {review.overall_sentiment} ({review.overall_score:+.1f})")
for aspect in review.aspects:
print(f" [{aspect.score:+.1f}] {aspect.aspect}: {aspect.sentiment}")
Classifying Text into Multiple Categories
Unlike simple classification, where a single label is assigned, multi-label classification recognizes that a text can belong to several categories. The incident described below touches on security, finance, fraud, and data protection simultaneously: forcing a single label would impoverish the routing logic.
Two elements of the schema deserve attention. reasoning requires a justification for each label, which improves score quality. requires_human_review turns model uncertainty into an operational signal: instead of discovering after the fact that a classification was dubious, you automatically escalate it to a human when confidence drops below 0.7.
from pydantic import BaseModel, Field
class LabelScore(BaseModel):
label: str = Field(description="Category name")
confidence: float = Field(description="Confidence score between 0.0 and 1.0")
reasoning: str = Field(description="One-sentence justification")
class MultiLabelClassification(BaseModel):
primary_label: str = Field(description="Primary category")
all_labels: list[LabelScore] = Field(
description="All applicable categories with their confidence scores"
)
requires_human_review: bool = Field(
description="True if classification is uncertain (confidence < 0.7)"
)
text = """
Our mobile payment application has a security vulnerability allowing
access to banking data. Several customers have reported unauthorized
transactions. The technical team is currently investigating.
"""
response = client.chat.parse(
model="mistral-large-latest",
messages=[
{
"role": "system",
"content": """Classify this text among the following categories (multiple possible):
security, finance, technical, legal, customer_support, compliance, fraud, data_privacy.
Assign a confidence score to each applicable category."""
},
{"role": "user", "content": text}
],
response_format=MultiLabelClassification,
temperature=0
)
result = response.choices[0].message.parsed
print(f"Primary category: {result.primary_label}")
print(f"Human review required: {'Yes' if result.requires_human_review else 'No'}")
for label in result.all_labels:
print(f" [{label.confidence:.0%}] {label.label} — {label.reasoning}")
Factorizing the Call
These three examples share the same mechanics: a system prompt, a text, a schema, and temperature=0. Instead of copying the call for every new case, extract it into a generic function. The TypeVar bound to BaseModel preserves typing: the returned object has the type of the schema passed as an argument, and your IDE knows it.
from typing import TypeVar, Type
from pydantic import BaseModel
T = TypeVar("T", bound=BaseModel)
def extract(client, text: str, schema: Type[T], system_prompt: str = "") -> T:
"""Reusable structured extraction."""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": text})
response = client.chat.parse(
model="mistral-large-latest",
messages=messages,
response_format=schema,
temperature=0
)
return response.choices[0].message.parsed
# Usage
invoice = extract(client, invoice_text, InvoiceData, "Extract invoice data.")
review = extract(client, customer_review, ReviewAnalysis, "Analyze this customer review.")
classification = extract(client, ticket, MultiLabelClassification, "Classify this text.")
Three very different use cases, each requiring only a single line of code to call: this is the concrete benefit of good schema modeling.
Key Takeaways
- Nested schemas (
list[LineItem],list[AspectSentiment]) enable complex analyses - Enums (
Currency, categories) constrain possible values - The
evidence/reasoningfield forces the model to justify its choices - A generic wrapper reduces code duplication
temperature=0is mandatory for these production use cases