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.create accepts a response_model and returns a validated Pydantic instance instead of raw text, retrying on schema validation failure.
  • Pydantic BaseModel: the typed schema class whose fields (with Field(description=...)) are serialized into the JSON schema Instructor sends to the LLM, guiding extraction and enforcing types on the response.
  • DocumentMetadata: the concrete BaseModel subclass 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 Pydantic class to 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.

Loading diagram...

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 DocumentType enum and each Field(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 with instructor.from_anthropic() and instructor.from_google()) so that response_model=DocumentMetadata binds the schema and max_retries caps how many times Instructor re-prompts with the validation error before raising. Every successful return is already a validated DocumentMetadata instance — no manual json.loads or 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 )
  • PartialMetadata provides 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 union return type lets callers branch on isinstance and 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

  1. Define a Pydantic BaseModel per extraction target and attach Field(description=...) to every field so Instructor can pass that intent into the JSON schema sent to the LLM.
  2. Constrain categorical fields with Enum types (like DocumentType) so the LLM cannot invent labels that break downstream consumers.
  3. Set a finite max_retries on client.chat.completions.create(...) so Instructor re-prompts on validation failure but the call still terminates.

Don'ts

  1. Don't parse raw LLM text with json.loads and hand-rolled validators — let Instructor's response_model enforce the schema.
  2. Don't omit Field(description=...) on extraction fields; without it the LLM has no hint about what to put there and extraction quality drops.
  3. 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

All free lessons in GenAI Data Engineering