Building reliable software often means ensuring that the data we work with follows strict formats or "schemas". Before the recent OpenAI update about structured output support, it didn’t guarantee that the data would always fit the exact structure developers needed.
On August 6, 2024, OpenAI announced support for Structured Outputs in the API. This new feature is designed to ensure that the data generated by OpenAI models perfectly matches the JSON Schemas you provide. Structured Outputs make it easier for developers to create applications that require precise and structured data, whether it’s for building smart assistants, automating data entry, or creating complex workflows.
In OpenAI tests, the latest model, gpt-4o-2024-08-06, using Structured Outputs, achieved a perfect score in following complex JSON schemas, compared to less than 40% for the older gpt-4-0613 model. This shows just how much more reliable and accurate Structured Outputs are in the latest update.
Why is Structured Outputs important?
Consistency in AI-generated outputs is crucial when integrating with different systems. When building applications that rely on AI, having data that adheres to a specific structure ensures smoother and more reliable integration. Without consistent output, developers often need to add extra layers of validation and correction, which can complicate the process and introduce potential points of failure. Structured Outputs eliminate this uncertainty by guaranteeing that the data generated by the AI models fits the exact schema you’ve defined. This makes it easier to plug AI-generated data directly into your systems without worrying about mismatches or errors.
This consistency also leads to more stable integrations. When you can trust that the data from OpenAI will always follow the same structure, you can confidently build workflows that depend on that data. Whether it’s feeding information into a database, triggering actions in another application, or performing complex multi-step processes, Structured Outputs ensure that every piece of data will be just as you expect, reducing the need for additional processing and making your entire system more efficient and reliable.
In this post, we'll make a simple example of how to use Structured Outputs. Basically, here is a high level overview of how it works.

Study Plan Generator Example
In this example, we'll make a simple Study Plan Generator sends a request to the OpenAI model gpt-4o-2024-08-06 to create a study plan. The OpenAI model returns a consistent JSON format as we specified in the response_format parameter. We pass the pydantic model StudyPlan so OpenAI will know that it needs to adhere to this format when sending back the response.
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
max_tokens=1024,
temperature=0,
messages=[
{"role": "system", "content": "You are a helpful math tutor and is an expert in creating study plans for homeschooling kids."},
{"role": "user", "content": "create a study plan to learn Algebra basics to my 14 year old"},
],
response_format=StudyPlan,
)
Then the JSON data from the OpenAI response, containing all the structured information of the study plan, is then used to produce both a PDF document and an HTML file.

Key Components
Pydantic Models (Lesson, Module, StudyPlan):
The code defines several Pydantic models to structure the data:
class Lesson(BaseModel):
title: str
description: str
duration_minutes: int
class Module(BaseModel):
title: str
description: str
objectives: List[str]
lessons: List[Lesson]
class StudyPlan(BaseModel):
name: str
start_date: str
end_date: str
notes: List[str]
modules: List[Module]
These models ensure that the AI's output conforms to specific attributes, such as title, description, and lessons for each module in the study plan. This structuring is crucial for consistent and reliable data integration.
Interacting with OpenAI:
The code initializes an OpenAI client and sends a request to generate a study plan.
client = OpenAI()
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
max_tokens=1024,
temperature=0,
messages=[
{"role": "system", "content": "You are a helpful math tutor and is an expert in creating study plans for homeschooling kids."},
{"role": "user", "content": "create a study plan to learn Algebra basics to my 14 year old"},
],
response_format=StudyPlan,
)
Here, the model is instructed to act as a math tutor and create a study plan for Algebra. The use of response_format=StudyPlan indicates that the model’s output should strictly follow the StudyPlan schema defined earlier.
Parsing and Validating the Response:
Once the AI generates the study plan, the response is parsed and validated against the StudyPlan schema:
event = completion.choices[0].message.parsed
json_string = event.json()
try:
study_plan_dict = json.loads(json_string)
study_plan = StudyPlan.model_validate(study_plan_dict)
print("The JSON string is valid and adheres to the StudyPlan schema.")
except Exception as e:
print("Validation error:", e)
The StudyPlan.model_validate() function checks if the JSON string conforms to the expected schema, ensuring that the output is both valid and consistent.
Converting and Saving the JSON:
The validated study plan is then converted to a formatted JSON string and saved to a file:
formatted_json = json.dumps(json.loads(json_string), indent=4)
with open("study_plan.json", 'w') as json_file:
json_file.write(formatted_json)
print(formatted_json)
This step ensures that the AI-generated study plan is saved in a human-readable and structured format, ready for further use or sharing.
Generating HTML and PDF Outputs:
Finally, the code converts the JSON data into HTML and PDF formats:
generate_html(formatted_json, "study_plan.html")
generate_pdf(formatted_json, "study_plan.pdf")
These functions likely take the structured JSON and format it into visually appealing documents, enhancing the accessibility and utility of the study plan.
Full Source Code
main.py
from pydantic import BaseModel
from typing import List, Optional
from openai import OpenAI
import json
from pydantic import BaseModel
from typing import List, Optional
from output_generators import generate_html, generate_pdf
class Lesson(BaseModel):
title: str
description: str
duration_minutes: int
class Module(BaseModel):
title: str
description: str
objectives: List[str]
lessons: List[Lesson]
class StudyPlan(BaseModel):
name: str
start_date: str
end_date: str
notes: List[str]
modules: List[Module]
client = OpenAI()
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
max_tokens=1024,
temperature=0,
messages=[
{"role": "system", "content": "You are a helpful math tutor and is an expert in creating study plans for homeschooling kids."},
{"role": "user", "content": "create a study plan to learn Algebra basics to my 14 year old"},
],
response_format=StudyPlan,
)
event = completion.choices[0].message.parsed
json_string = event.json()
try:
study_plan_dict = json.loads(json_string)
study_plan = StudyPlan.model_validate(study_plan_dict)
print("The JSON string is valid and adheres to the StudyPlan schema.")
except Exception as e:
print("Validation error:", e)
formatted_json = json.dumps(json.loads(json_string), indent=4)
with open("study_plan.json", 'w') as json_file:
json_file.write(formatted_json)
print(formatted_json)
generate_html(formatted_json, "study_plan.html")
generate_pdf(formatted_json, "study_plan.pdf")
output_generators.py
from fpdf import FPDF
from typing import Dict
import json
from pathlib import Path
def generate_html(json_data: str, output_file: str) -> None:
study_plan = json.loads(json_data)
html_content = f"""
<html>
<head>
<title>{study_plan['name']}</title>
<style>
body {{
font-family: 'Arial', sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
line-height: 1.6;
}}
.container {{
max-width: 800px;
margin: auto;
background: white;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}}
h1 {{
color: #333;
font-size: 24px;
border-bottom: 2px solid #ddd;
padding-bottom: 10px;
}}
h2 {{
color: #555;
font-size: 20px;
margin-top: 40px;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
}}
h3 {{
color: #666;
font-size: 18px;
margin-top: 20px;
}}
p {{
color: #444;
}}
ul {{
list-style-type: disc;
padding-left: 20px;
color: #444;
}}
li {{
margin-bottom: 10px;
}}
.module {{
margin-bottom: 30px;
padding: 10px;
background-color: #f9f9f9;
}}
.lesson {{
margin-left: 20px;
padding-left: 10px;
}}
.label {{
font-weight: bold;
color: #333;
margin-top: 10px;
}}
</style>
</head>
<body>
<div class="container">
<h1>Study Plan: {study_plan['name']}</h1>
<p><strong>Start Date:</strong> {study_plan['start_date']}</p>
<p><strong>End Date:</strong> {study_plan['end_date']}</p>
<h2>Notes</h2>
<ul>
"""
for note in study_plan['notes']:
html_content += f"<li>{note}</li>"
html_content += "</ul><h2>Modules</h2>"
for module in study_plan['modules']:
html_content += f"""
<div class="module">
<h3>{module['title']}</h3>
<p>{module['description']}</p>
<div class="label">Objectives</div>
<ul>
"""
for objective in module['objectives']:
html_content += f"<li>{objective}</li>"
html_content += "</ul> <div class='label'>Lessons</div><div class='lesson'><ul>"
for lesson in module['lessons']:
html_content += f"""
<li>
<strong>{lesson['title']}</strong><br>
{lesson['description']}<br>
<div class="label">Duration:</div>
<em>{lesson['duration_minutes']} minutes</em>
</li>
"""
html_content += "</ul></div></div>"
html_content += """
</div>
</body>
</html>
"""
with open(output_file, 'w') as file:
file.write(html_content)
def generate_pdf(json_data: str, output_file: str) -> None:
study_plan = json.loads(json_data)
pdf = FPDF()
pdf.set_auto_page_break(auto=True, margin=15)
pdf.add_page()
pdf.set_font("Arial", 'B', 16)
pdf.cell(0, 10, f"Study Plan: {study_plan['name']}", 0, 1, 'C')
pdf.set_font("Arial", '', 12)
pdf.cell(0, 10, f"Start Date: {study_plan['start_date']}", 0, 1)
pdf.cell(0, 10, f"End Date: {study_plan['end_date']}", 0, 1)
pdf.set_font("Arial", 'B', 14)
pdf.cell(0, 10, "Notes:", 0, 1)
pdf.set_font("Arial", '', 12)
for note in study_plan['notes']:
pdf.multi_cell(0, 10, f"- {note}")
pdf.set_font("Arial", 'B', 14)
pdf.cell(0, 10, "Modules:", 0, 1)
for module in study_plan['modules']:
pdf.set_font("Arial", 'B', 12)
pdf.set_fill_color(240, 240, 240)
pdf.cell(0, 10, f"Module Title: {module['title']}", 0, 1, 'L', fill=True)
pdf.set_font("Arial", '', 12)
pdf.multi_cell(0, 10, f"Description: {module['description']}")
pdf.set_font("Arial", 'I', 12)
pdf.cell(0, 10, "Objectives:", 0, 1)
for objective in module['objectives']:
pdf.multi_cell(0, 10, f"- {objective}")
pdf.set_font("Arial", '', 12)
pdf.cell(0, 10, "Lessons:", 0, 1)
for lesson in module['lessons']:
pdf.set_fill_color(255, 240, 240)
pdf.cell(0, 10, f" Lesson Title: {lesson['title']}", 0, 1, 'L', fill=True)
pdf.multi_cell(0, 10, f" Description: {lesson['description']}")
pdf.cell(0, 10, f" Duration: {lesson['duration_minutes']} minutes", 0, 1)
pdf.ln(5)
pdf.ln(10)
pdf.output(output_file)
requirements.txt
annotated-types==0.7.0
anyio==4.4.0
certifi==2024.7.4
distro==1.9.0
fpdf==1.7.2
h11==0.14.0
httpcore==1.0.5
httpx==0.27.0
idna==3.7
jiter==0.5.0
openai==1.40.6
pydantic==2.8.2
pydantic_core==2.20.1
sniffio==1.3.1
tqdm==4.66.5
typing_extensions==4.12.2
Running the example
Installing the dependencies using pip install then run the application.
pip install -r requirements.txt
python main.py
With Structured Outputs, we can now confidently build and integrate AI-driven solutions that are both precise and dependable, saving time and reducing the need for extensive validation or error handling. This new feature of OpenAI will make integration with different systems seamless and reliable.