Reusable Structured Output Fixer for LangChain Using Pydantic

Introduction/ Issue:  

One of the biggest challenges when working with Large Language Models (LLMs) is obtaining reliable structured output.

Even if you explicitly ask an LLM to return JSON, the response isn’t always in the format your application expects. Sometimes it wraps the JSON inside Markdown code blocks, includes additional explanations, misses required fields, or returns values with incorrect data types.

These issues are easy to overlook during experimentation, but they can quickly become a problem in production applications where APIs, databases, or downstream services expect data in a consistent structure.

A common solution is to validate the response after receiving it. However, instead of immediately throwing an exception when validation fails, we can give the LLM an opportunity to correct its own output.

That’s exactly what we’ll build in this article.

We’ll create a reusable Structured Output Fixer using LangChain and Pydantic. This utility validates the LLM’s response against a predefined schema and, whenever the response is invalid, automatically sends the validation error back to the model. The LLM then attempts to generate a corrected response, making the entire process more reliable without adding extra complexity to your application.

Since the component accepts any LangChain chat model, you can use it with Groq, OpenAI, Anthropic, Google Gemini, Ollama, or any other compatible provider.

Since the component accepts any LangChain chat model, you can use it with Groq, OpenAI, Anthropic, Google Gemini, Ollama, or any other compatible provider.

Why we need to do / Cause of the issue: 

Instead of implementing validation and retry logic in every project, you can build this component once and reuse it across all your LangChain applications.

This component:

  • Works with any LangChain chat model.
  • Validates responses using Pydantic.
  • Removes Markdown code fences from LLM responses.
  • Automatically retries when validation fails.
  • Returns strongly typed Python objects.
  • Keeps your application code clean, reusable, and easier to maintain.

By centralizing this logic into a reusable component, you improve reliability, reduce duplicate code, and make your AI applications more production-ready.

How do we solve:

  1. Send the user’s request along with the expected JSON schema to the LLM.
  2. Extract the JSON object from the response.
  3. Validate it using a Pydantic model.
  4. If the validation succeeds, return the structured object.
  5. If validation fails, send the validation error back to the LLM and ask it to fix the response.
  6. Repeat the process until the output is valid or the retry limit is reached.

Instead of failing immediately, the model gets an opportunity to correct its own mistakes, resulting in much more reliable structured output.

Step 1: Create a project folder and Install the required packages in the terminal using the below command.

pip install langchain langchain-core langchain-groq pydantic python-dotenv

structured-output-fixer/

├── structured_output_fixer.py

└── .env

 

Step 2: Create a environment file using .env using and paste your LLM API Key

Step -3 :Create a file named structured_output_fixer.py and paste the complete code from this article into it.

 

“””

Structured Output Fixer

 

A reusable utility that validates LLM responses against a Pydantic schema.

If the generated response is invalid, the validation error is sent back to

the model, allowing it to correct its own output automatically.

“””

from __future__ import annotations

import json

import re

from typing import Generic, Type, TypeVar

from pydantic import BaseModel, ValidationError

from langchain_core.language_models.chat_models import BaseChatModel

from langchain_core.messages import HumanMessage, SystemMessage

T = TypeVar(“T”, bound=BaseModel)

class StructuredOutputError(Exception):

“””Raised when a valid structured response cannot be generated.”””

def __init__(self, message: str, last_raw_output: str, last_error: str):

super().__init__(message)

self.last_raw_output = last_raw_output

self.last_error = last_error

class StructuredOutputFixer(Generic[T]):

“””

Ensures that LLM responses conform to a Pydantic schema by

validating the generated JSON and automatically requesting

corrections whenever validation fails.

“””

def __init__(

self,

llm: BaseChatModel,

schema: Type[T],

max_retries: int = 3,

verbose: bool = False,

):

self.llm = llm

self.schema = schema

self.max_retries = max_retries

self.verbose = verbose

def _log(self, message: str):

“””

Prints debug messages when verbose mode is enabled.

“””

if self.verbose:

print(f”[StructuredOutputFixer] {message}”)

def _system_prompt(self) -> str:

“””

Builds the system prompt containing the JSON schema that

the LLM must follow.

“””

schema_json = json.dumps(

self.schema.model_json_schema(),

indent=2,

)

return (

“You are an information extraction assistant.\n\n”

“Return ONLY a valid JSON object.\n”

“Do not include markdown, explanations or extra text.\n\n”

f”JSON Schema:\n{schema_json}”

)

def _initial_messages(self, user_input: str):

“””

Creates the initial conversation sent to the LLM.

“””

return [

SystemMessage(content=self._system_prompt()),

HumanMessage(content=user_input),

]

def _retry_messages(

self,

user_input: str,

previous_output: str,

validation_error: str,

):

“””

Creates a retry prompt by including the previous response

and the corresponding validation error.

“””

return [

SystemMessage(content=self._system_prompt()),

HumanMessage(

content=f”””

Original Request

{user_input}

Your previous response

{previous_output}

Validation Error

{validation_error}

Please correct your previous response.

Return ONLY the corrected JSON object.

“””

),

]

@staticmethod

def _extract_json(text: str) -> str:

“””Extracts the JSON object from the LLM response.”””

text = text.strip()

text = re.sub(

r”^“`(?:json)?\s*|\s*“`$”,

“”,

text,

flags=re.MULTILINE,

)

match = re.search(r”\{.*\}”, text, re.DOTALL)

return match.group(0) if match else text

def run(self, user_input: str) -> T:

“””

Generates, validates and returns structured output.

“””

messages = self._initial_messages(user_input)

last_raw_output = “”

last_error = “”

for attempt in range(1, self.max_retries + 1):

self._log(f”Attempt {attempt}/{self.max_retries}”)

response = self.llm.invoke(messages)

last_raw_output = response.content

cleaned_json = self._extract_json(last_raw_output)

try:

parsed = json.loads(cleaned_json)

validated = self.schema.model_validate(parsed)

self._log(“Validation successful.”)

return validated

except (json.JSONDecodeError, ValidationError) as ex:

last_error = str(ex)

self._log(“Validation failed.”)

self._log(last_error)

messages = self._retry_messages(

user_input=user_input,

previous_output=last_raw_output,

validation_error=last_error,

)

raise StructuredOutputError(

message=(

f”Failed to generate a valid ”

f”{self.schema.__name__} ”

f”after {self.max_retries} attempts.”

),

last_raw_output=last_raw_output,

last_error=last_error,

)

# ———————————————————————-

# Example Usage

# ———————————————————————-

if __name__ == “__main__”:

from dotenv import load_dotenv

from langchain_groq import ChatGroq

load_dotenv()

class Invoice(BaseModel):

“””

Represents the structured invoice information extracted from

unstructured text.

“””

invoice_number: str

vendor_name: str

invoice_date: str

total_amount: float

currency: str

llm = ChatGroq(

model=”llama-3.3-70b-versatile”,

temperature=0,

)

fixer = StructuredOutputFixer(

llm=llm,

schema=Invoice,

max_retries=3,

verbose=True,

)

invoice = fixer.run(

“””

Extract invoice details.

Invoice Number: INV-2026-1045

Vendor: ABC Technologies Pvt Ltd

Invoice Date: 30 July 2026

Amount Due: USD 1589.50

“””

)

print(“\nValidated Output\n”)

print(invoice)

Step 5: Run the file

python structured_output_fixer.py

Expected Output

You should see logs similar to the following:

[StructuredOutputFixer] Attempt 1/3

[StructuredOutputFixer] Validation successful.

Conclusion: 

Structured output is a common requirement in modern AI applications, but relying on an LLM to always return perfectly formatted JSON is risky. Even well-crafted prompts can produce responses that are missing fields, contain invalid JSON, or include unwanted formatting.

By introducing a reusable Structured Output Fixer, you can validate every response against a Pydantic schema and automatically allow the model to correct its own mistakes before the data reaches your application.

Because the component is model-agnostic, it can be used with any LangChain-supported chat model, making it easy to integrate into existing and future projects. More importantly, it encapsulates validation, correction, and retry logic into a single reusable utility, resulting in cleaner application code and more reliable AI workflows.

I hope this component helps simplify your LangChain projects and serves as a useful building block for creating robust, production-ready GenAI applications.

Recent Posts