Free lesson · GenAI Data Engineering
Extract structured metadata using Instructor with Pydantic schemas
Use Instructor to define Pydantic models for metadata (titles, authors, dates, entities) and extract with automatic validation, retries, and multi-provider support.
Course: GenAI Data Pipelines · Chapter 4 · Context Engineering & LLM Enrichment
Free to read — no subscription required.
Introduction
When you index thousands of documents into a search system, raw text alone isn't enough — downstream consumers need typed fields they can filter, facet, and join, and the moment an LLM returns malformed JSON your enrichment job either crashes or silently drops the document. This lesson shows how to extract reliable, schema-validated metadata from raw documents using Instructor with Pydantic models. By the end you'll be able to define typed extraction schemas, wire Instructor into an LLM client so every call returns a validated Pydantic instance, and add a fallback path so malformed inputs never silently drop out of your enrichment pipeline.
Key Terminology
- Instructor: a library that patches LLM client SDKs (OpenAI, Anthropic, Google) so
chat.completions.createaccepts aresponse_modeland returns a validated Pydantic instance instead of raw text, retrying on schema validation failure. - Pydantic
BaseModel: the typed schemaclasswhose fields (withField(description=...)) are serialized into the JSON schema Instructor sends to the LLM, guiding extraction and enforcing types on the response. DocumentMetadata: the concreteBaseModelsubclass defined in this lesson — title, authors, type, dates, keywords — that the LLM must populate per document; it is the JSON-schema target for every extraction call and the unit of output for the downstream index.response_model+max_retries: the Instructor call parameters that bind a Pydanticclassto the LLM response and bound how many times Instructor re-prompts with the validation error before raising.
Concepts
Reliable metadata extraction rests on three layers. First, a typed Pydantic schema (with Field descriptions and Enum constraints) tells the LLM exactly which fields to produce and what shape they must take. Second, Instructor-patched LLM calls return parsed, validated Pydantic instances — and on a validation failure, automatically re-prompt the model with the validation error so it can correct itself within a max_retries budget. Third, a graceful fallback wraps the call so documents that exhaust the retry budget or hit API errors still emit a PartialMetadata record rather than disappearing. Together these eliminate fragile JSON parsing, prevent invented enum labels, and keep the pipeline's output uniformly machine-readable.
Code Walkthrough
Defining the Schema and Wiring Instructor
The first step in building a reliable extraction pipeline is designing Pydantic models that define exactly what metadata fields the LLM should extract, and then patching the OpenAI client with Instructor so every call returns a validated DocumentMetadata instance rather than raw text. The DocumentType enum constrains classification to a controlled set of categories so the LLM cannot invent labels, each Field(description=...) is serialized into the JSON schema sent to the LLM to guide extraction, and instructor.from_openai() patches the client so chat.completions.create accepts response_model and max_retries for automatic retry on validation failure.
Code snippetpython
1import instructor 2from openai import OpenAI 3from pydantic import BaseModel, Field 4from typing import Optional 5from datetime import date 6from enum import Enum 7 8class DocumentType(str, Enum): 9 RESEARCH_PAPER = "research_paper" 10 TECHNICAL_REPORT = "technical_report" 11 BLOG_POST = "blog_post" 12 DOCUMENTATION = "documentation" 13 LEGAL = "legal" 14 FINANCIAL = "financial" 15 16class Author(BaseModel): 17 name: str 18 affiliation: Optional[str] = None 19 20class DocumentMetadata(BaseModel): 21 title: str = Field(description="The document's main title") 22 authors: list[Author] = Field(default_factory=list, description="Document authors") 23 document_type: DocumentType = Field(description="Classification of document type") 24 publication_date: Optional[date] = Field(None, description="Publication or creation date") 25 abstract: Optional[str] = Field(None, description="Brief summary, max 200 words") 26 keywords: list[str] = Field(default_factory=list, description="Key topics, max 10") 27 language: str = Field(default="en", description="ISO 639-1 language code") 28 29client = instructor.from_openai(OpenAI()) 30 31def extract_metadata(text: str, max_retries: int = 3) -> DocumentMetadata: 32 return client.chat.completions.create( 33 model="gpt-4o-mini", 34 response_model=DocumentMetadata, 35 max_retries=max_retries, 36 messages=[ 37 { 38 "role": "system", 39 "content": "Extract document metadata from the provided text. Be precise with dates and author names.", 40 }, 41 {"role": "user", "content": text[:4000]}, 42 ], 43 )
- The
DocumentTypeenum and eachField(description=...)become part of the JSON schema Instructor sends to the LLM, restricting categorical output to valid values and guiding the model on what each field means.Field(default_factory=list)supplies safe empty defaults when the LLM omits an optional list field. instructor.from_openai()patches the OpenAI client (the same pattern works withinstructor.from_anthropic()andinstructor.from_google()) so thatresponse_model=DocumentMetadatabinds the schema andmax_retriescaps how many times Instructor re-prompts with the validation error before raising. Every successfulreturnis already a validatedDocumentMetadatainstance — no manualjson.loadsor hand-rolled validator.
Handling Extraction Failures
Not every document yields clean extraction results, especially when dealing with malformed text, unusual formats, or ambiguous content that exhausts the LLM's retry budget. The extract_with_fallback function below wraps the primary extraction call in a try/except block that catches both Pydantic validation errors and API errors, returning a PartialMetadata instance with sensible defaults and an error log. This ensures your enrichment pipeline never drops documents silently — every input produces a result, even if partial.
Code snippetpython
1from pydantic import ValidationError 2 3class PartialMetadata(BaseModel): 4 title: str = "Unknown" 5 document_type: DocumentType = DocumentType.DOCUMENTATION 6 extraction_confidence: float = Field(default=0.0, ge=0.0, le=1.0) 7 extraction_errors: list[str] = Field(default_factory=list) 8 9def extract_with_fallback(text: str) -> DocumentMetadata | PartialMetadata: 10 try: 11 return extract_metadata(text, max_retries=3) 12 except (ValidationError, Exception) as e: 13 return PartialMetadata( 14 extraction_errors=[str(e)], 15 extraction_confidence=0.0, 16 )
PartialMetadataprovides a degraded extraction result with sensible defaults and an error log. Documents that fail full extraction still produce usable metadata rather than being silently dropped — the unionreturntype lets callers branch onisinstanceand route degraded records to a review queue.- The
except (ValidationError, Exception)catches both schema-mismatch failures (after all retries are exhausted) and transport-level errors (network failures, rate limits, model errors), so every extraction attempt produces a record.
You'll know it works when extract_metadata on a clean document returns a fully populated DocumentMetadata, and extract_with_fallback on a malformed input returns a PartialMetadata with extraction_errors populated rather than raising.
Do's and Don'ts
Having just walked through the schema, the Instructor wiring, and the fallback path, the rules below distil that into checks you can apply to any new extraction target.
Do's
- ✓Define a
Pydantic BaseModelper extraction target and attachField(description=...)to every field so Instructor can pass that intent into the JSON schema sent to the LLM. - ✓Constrain categorical fields with
Enumtypes (likeDocumentType) so the LLM cannot invent labels that break downstream consumers. - ✓Set a finite
max_retriesonclient.chat.completions.create(...)so Instructor re-prompts on validation failure but the call still terminates.
Don'ts
- ✗Don't parse raw LLM text with
json.loadsand hand-rolled validators — let Instructor'sresponse_modelenforce the schema. - ✗Don't omit
Field(description=...)on extraction fields; without it the LLM has no hint about what to put there and extraction quality drops. - ✗Don't use unbounded
max_retries(or skip it) — a malformed input can spin against the LLM indefinitely and blow your API budget.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Data Engineering subscription.
From · cancel anytime · Already a subscriber? Sign in →
More free lessons in GenAI Data Pipelines
- Ch 1Design a unified document model normalizing all extraction outputs
- Ch 1Build a routing system selecting the optimal extraction method
- Ch 2Implement content quality scoring with NeMo Curator filters
- Ch 3Build Anthropic's Contextual Retrieval pattern
- Ch 4Extract structured metadata using Instructor with Pydantic schemasYou are here
- Ch 5Design multi-format storage strategies on GCS and PostgreSQL
- Ch 6Build an embedding benchmarking framework