Extract Fields from Text
What Extraction Is
Extraction means pulling specific pieces of information out of text that already exists: the order number in an email, the date in an announcement, the total on an invoice. The text is the source, and the output must be grounded in it.
Extraction is the most common use of structured outputs in real applications, because businesses already have large amounts of unstructured text: emails, forms, reports, transcripts, and web pages. A schema turns that text into records a database or workflow can use.
"Hi, this is Ravi Kumar (order ORD-20391). Please send 2 Python Basics by 20 November 2026."
│
▼ extraction schema
{"customer_name": "Ravi Kumar", "order_number": "ORD-20391",
"items": [{"product": "Python Basics", "quantity": 2}],
"requested_delivery_date": "2026-11-20"}
Lesson 106 focuses on extraction: getting facts from a source. Lesson 107 covers generation: creating new data that does not come from a source. The distinction matters, because the rules differ.
Extraction Rules
Good extraction follows four rules, and the schema and prompt should enforce them together:
| Rule | Why | How |
|---|---|---|
| Use only what the source states | Invented values look correct and are dangerous | System prompt: "Use only information the text states" |
| Use null for missing values | Guessing fills gaps silently | Nullable fields, with a clear instruction |
| Normalize to a standard form | Downstream code needs one format | Describe the format: YYYY-MM-DD, whole rupees, exact order numbers |
| Ground the answer in evidence | Makes errors checkable | An evidence field with a quote copied from the source |
Missing Values: Null, Not Guesses
Many extraction failures are not wrong formats, but plausible guesses. If an email says "next Tuesday", the extractor should not invent a calendar date. Make missing values explicit:
requested_delivery_date: str | None = Field(
description="Requested date in YYYY-MM-DD format, or null if the text does not state a date"
)
The description explains both the format and the missing-value behavior. Claude sees the instruction, and the schema guarantees the field exists (Lesson 104).
Lists: Every Instance, or None
For repeated information, such as products in an order, use a list. Tell Claude to include every instance, and to return an empty list when there are none:
items: list[OrderItem] = Field(description="Every product the customer asks for; empty list if none")
Without the instruction, models sometimes summarize several products into one, or miss the last one.
Grounding with Evidence
A field such as customer_name can be wrong without any visible sign. An evidence field makes the extraction checkable: Claude copies a sentence from the source that supports the result, and your code confirms that the sentence really appears in the text:
evidence: str = Field(description="One sentence copied exactly from the email that shows the order request")
If the "evidence" is not in the source, the extraction is suspect. This check is cheap, objective, and catches many invented values. It does not prove every field correct, but it turns a hidden error into a visible one.
Note: the evidence check compares text after normalizing whitespace and case. It can reject a correct quote that Claude paraphrased, so use it as a signal for review or retry, not as the only truth.
Practical Example: Order Extraction from Emails
This example extracts order details from customer emails. It checks that dates are valid and quantities are positive, and verifies that the evidence quote appears in the email.
"""order_extraction.py: extract order details from customer emails."""
from datetime import date
from anthropic import Anthropic
from pydantic import BaseModel, Field, ValidationError
client = Anthropic()
MODEL = "claude-opus-5-5"
SYSTEM = (
"You extract order details from customer emails. Use only information the email states. "
"Never guess or infer a missing value: use null for any field the email does not provide. "
"For the evidence field, copy one short sentence from the email exactly as written."
)
class OrderItem(BaseModel):
product: str = Field(description="Product name, as written in the email")
quantity: int | None = Field(description="Whole number of units; null if no quantity is stated")
class OrderEmail(BaseModel):
customer_name: str | None = Field(description="Sender's full name, or null if not given")
order_number: str | None = Field(description="Order number exactly as written, or null")
items: list[OrderItem] = Field(description="Every product the customer asks for; empty list if none")
requested_delivery_date: str | None = Field(description="Requested date in YYYY-MM-DD format, or null")
evidence: str = Field(description="One sentence copied exactly from the email that shows the order request")
def normalize(text: str) -> str:
return " ".join(text.split()).lower()
def check_order(order: OrderEmail, email_text: str) -> list[str]:
problems = []
if order.requested_delivery_date is not None:
try:
date.fromisoformat(order.requested_delivery_date)
except ValueError:
problems.append(f"delivery date {order.requested_delivery_date!r} is not YYYY-MM-DD")
for item in order.items:
if item.quantity is not None and item.quantity < 1:
problems.append(f"quantity for {item.product!r} must be at least 1")
if not order.evidence.strip():
problems.append("evidence is empty")
elif normalize(order.evidence) not in normalize(email_text):
problems.append("evidence is not a sentence from the email")
return problems
def extract_order(email_text: str) -> tuple[OrderEmail | None, list[str]]:
try:
response = client.messages.parse(
model=MODEL,
max_tokens=2048,
system=SYSTEM,
output_format=OrderEmail,
messages=[{"role": "user", "content": f"<email>\n{email_text}\n</email>"}],
)
except ValidationError as error:
return None, [f"response failed validation: {error.errors()[0]['msg']}"]
if response.stop_reason != "end_turn":
return None, [f"incomplete response (stop_reason={response.stop_reason})"]
order = response.parsed_output
return order, check_order(order, email_text)
EMAILS = [
"Hi, this is Ravi Kumar (order ORD-20391). Please send 2 copies of Python Basics and "
"1 SQL Workbook by Friday, 20 November 2026. Thanks!",
"Can you send the Django starter kit when you get a chance? Thanks, Meena",
]
if __name__ == "__main__":
for email_text in EMAILS:
order, problems = extract_order(email_text)
if order is None:
print("Not extracted:", problems)
continue
print(f"customer: {order.customer_name} order: {order.order_number}")
for item in order.items:
quantity = item.quantity if item.quantity is not None else "?"
print(f" {quantity} x {item.product}")
print(f" delivery: {order.requested_delivery_date}")
if problems:
print(" problems:", problems)
Expected output (exact wording varies):
customer: Ravi Kumar order: ORD-20391
2 x Python Basics
1 x SQL Workbook
delivery: 2026-11-20
customer: Meena order: None
? x Django starter kit
delivery: None
How It Works
SYSTEMstates the extraction rules: use only what the email says, use null for missing values, and copy the evidence exactly.OrderEmailcombines the rules with the schema. Every field is nullable or a list, so missing information has a defined representation. The descriptions specify formats, such asYYYY-MM-DD, and the missing-value behavior.- The email is wrapped in tags (Lesson 37), so its content is clearly data.
check_order()performs three checks the schema cannot: that the delivery date is a real date, that quantities are positive, and that the evidence quote appears in the email.- The second email has no order number, no quantity, and no date. Those fields are null, and the product is still extracted, with an unknown quantity shown as
?by the display code. - Completeness is checked first: a truncated or refused response returns no order, rather than partial data.
Common Mistakes
Letting the Model Infer Missing Values
What happens: "next Tuesday" becomes a specific date that nobody provided.
Fix: instruct Claude to return null when the text does not state a value.
Asking for a Single Value When Several Exist
What happens: only one product from an order is extracted.
Fix: use a list, and say "every" in the description.
Vague Formats
What happens: dates arrive as 20/11/26, Nov 20, and 20th November in the same batch.
Fix: specify the exact format in the description, and validate it in code.
Trusting Extraction Without Evidence
What happens: invented values pass unnoticed.
Fix: require evidence, and check that it appears in the source.
Extracting from Text That Is Too Long for One Pass
What happens: values from the middle of a long document are missed.
Fix: split the document into sections, extract from each, and merge the results in code (Lesson 109).
Best Practices
- Write the extraction rules in the system prompt: use only stated values, null when missing.
- Describe formats and missing-value behavior in every field.
- Use lists for repeated information, with "every" in the description.
- Add an evidence field and verify it in code.
- Validate formats and ranges that the schema cannot enforce.
- Use
Literaltypes for fixed categories. - Keep the source in tags, separate from instructions.
Exercise
- Add a
delivery_addressfield (nullable) toOrderEmail, and test it with an email that includes an address. - Write an email that mentions two different order numbers. What does the extraction return, and what check would you add?
- Change
check_order()to reject an order with items but no customer name, and explain whether this should be a schema rule or a code rule. - Explain why the evidence check uses
normalize()on both strings.
Step 2: Claude may choose one order number, or may return the one that appears first. The schema cannot express "exactly one", so add a code check that counts order-number-like patterns in the text, for example with a regular expression such as ORD-\d+, and flags emails with more than one match for human review.
Step 4: whitespace and capitalization can differ between a quote copied by Claude and the original email, for example line breaks inside a sentence or a capital letter at the start. Normalizing both strings means the check compares the words, not the layout, so correct quotes are not rejected for trivial differences.