Creating JSON-Style Structured Responses with Prompts
Why Structured Responses Matter for Developers When a model's output is read by a person, small variations in format do not matter. When it is read by code, they matter completely. A missing field, an extra sentence before the JSON, or "duration": "6 weeks" instead of "duration_weeks": 6 can break a feature.
Earlier lessons introduced the pieces: describing JSON formats in prompts (Lesson 8), designing extraction schemas and validating with Pydantic (Lesson 22), and using examples for structured output (Lesson 31). This lesson brings them together into a practical answer to a common developer question:
How do I reliably get structured data from a model into my application?
The answer depends on the level of guarantee you need.
Level How What Is Guaranteed
- Prompt-only JSON Describe the format in the prompt, then parse and validate Nothing; format is likely but must be checked
- JSON mode An API setting that makes the output valid JSON Valid JSON syntax, but not your fields or types
- Structured Outputs with a schema An API feature that constrains output to a JSON Schema Your fields, types, and allowed values (structure) Always: semantic validation Your own checks in code Correctness of values for your application Even the strongest level guarantees only structure. A schema can require a field called fee of type integer; it cannot guarantee that the number is the correct fee. Every level therefore ends with validation in code.
Level 1: Prompt-Only JSON Prompt-only JSON works with any model and any API. It requires a precise format description, robust parsing, and validation.
The Prompt Extract course details from the description in .
Return only a JSON object with exactly these keys:
- "title": string, exactly as written in the description
- "level": "Beginner", "Intermediate", "Advanced", or null if not stated
- "duration_weeks": integer, or null if not stated
- "topics": array of strings, or [] if none are listed
Do not add any text before or after the JSON. Do not use code fences.
{text} Parsing and Validating With Pydantic Requirements: pip install openai pydantic (Pydantic 2) and an OPENAI_API_KEY environment variable.
from typing import Literal
from openai import OpenAI from pydantic import BaseModel, Field, ValidationError
client = OpenAI() MODEL = "gpt-5.4" # Replace with a model available in your account
class CourseInfo(BaseModel): title: str = Field(description="Course title exactly as written") level: Literal["Beginner", "Intermediate", "Advanced"] | None = Field( description="Course level, or null if not stated" ) duration_weeks: int | None = Field(description="Duration in weeks, or null if not stated") topics: list[str] = Field(description="Topics covered; empty list if none are listed")
EXTRACT_INSTRUCTIONS = """Extract course details from the description in . Return only a JSON object with exactly these keys: "title" (string, exactly as written), "level" ("Beginner", "Intermediate", "Advanced", or null), "duration_weeks" (integer or null), "topics" (array of strings, [] if none). Do not add any text before or after the JSON. Do not use code fences."""
def validation_problems(error: ValidationError) -> list[str]: """Turn a Pydantic ValidationError into short, readable problem descriptions.""" return [f"{'.'.join(str(part) for part in item['loc']) or 'root'}: {item['msg']}" for item in error.errors()]
def extract_course(description: str, max_attempts: int = 2) -> CourseInfo: prompt = f"\n{description}\n" for _ in range(max_attempts): response = client.responses.create(model=MODEL, instructions=EXTRACT_INSTRUCTIONS, input=prompt) try: return CourseInfo.model_validate_json(response.output_text) except ValidationError as error: feedback = "\n".join(f"- {p}" for p in validation_problems(error)) prompt = (f"\n{description}\n\n\n" f"Your previous output had these problems:\n{feedback}\n" f"Return a corrected JSON object only.") raise ValueError("Could not obtain valid course details") Code Explanation CourseInfo A Pydantic model that defines the expected structure:
Literal[...] | None restricts level to the allowed values or None (JSON null). int | None allows a number or null. Each Field(description=...) documents the field. Because no default value is given, every field is required, even the ones that can be null. This means a missing key is an error, while a key with null is valid. model_validate_json() Parses the JSON text and validates it against the model in one step. Invalid JSON, missing fields, wrong types, and disallowed values all raise a ValidationError.
validation_problems() Each entry from error.errors() contains a loc (the location of the problem, such as ('level',)) and a msg (a description). Joining them gives precise feedback, such as which field has which problem.
Retry with feedback If validation fails, the next request includes the specific problems, which is far more effective than simply asking again (Lesson 73). After max_attempts, the function raises an error so the caller can handle the failure, for example by sending the item for human review.
When Level 1 Is Appropriate The model or API does not support schema-constrained output. You are prototyping. The format is simple and validation with retry is reliable enough in your tests. Level 2: JSON Mode Some APIs offer a JSON mode that guarantees the output is syntactically valid JSON. In the OpenAI Responses API, it is enabled with a text format setting:
response = client.responses.create( model=MODEL, instructions=EXTRACT_INSTRUCTIONS, # the instructions must mention JSON input=prompt, text={"format": {"type": "json_object"}}, ) What It Guarantees and What It Does Not Guaranteed Not Guaranteed The output parses as JSON The keys you asked for No text before or after the JSON The correct types and allowed values JSON mode removes parsing failures, but you still need the full validation from Level 1. Where schema-constrained output is available, prefer Level 3.
Level 3: Structured Outputs With a Schema Structured Outputs is an API feature that constrains the model so that its output matches a JSON Schema you provide. The OpenAI Python SDK can create the schema directly from a Pydantic model.
Using a Pydantic Model response = client.responses.parse( model=MODEL, instructions="Extract course details from the description. Use null for anything not stated.", input=f"\n{description}\n", text_format=CourseInfo, )
course = response.output_parsed if course is None: raise ValueError("The model did not return structured data") print(course.title, course.level, course.duration_weeks, course.topics) Code Explanation client.responses.parse() sends the request with a schema generated from CourseInfo and parses the result. text_format=CourseInfo tells the SDK which Pydantic model defines the output. response.output_parsed contains a CourseInfo instance, so you work with typed attributes instead of dictionary keys. Checking for None handles cases where no parsed result is available, for example when the model declines to answer. Your application must handle this case explicitly. Notice how much shorter the instructions are. The format no longer needs to be described in words, because the schema enforces it. The instructions focus on content rules, such as using null for missing information.
Using a JSON Schema Directly Without Pydantic, or in other languages, pass the schema in the request's text.format setting:
import json
COURSE_SCHEMA = { "type": "object", "properties": { "title": {"type": "string"}, "level": {"type": ["string", "null"], "enum": ["Beginner", "Intermediate", "Advanced", None]}, "duration_weeks": {"type": ["integer", "null"]}, "topics": {"type": "array", "items": {"type": "string"}}, }, "required": ["title", "level", "duration_weeks", "topics"], "additionalProperties": False, }
response = client.responses.create( model=MODEL, instructions="Extract course details from the description. Use null for anything not stated.", input=f"\n{description}\n", text={"format": {"type": "json_schema", "name": "course_info", "strict": True, "schema": COURSE_SCHEMA}}, ) data = json.loads(response.output_text) Schema Rules for Strict Mode In OpenAI's strict mode:
Every property must be listed in required. Optional values are expressed as nullable types, such as ["integer", "null"], rather than by leaving the field out. additionalProperties must be false, so no unexpected keys appear. Not every JSON Schema feature is supported. Check the provider's Structured Outputs documentation for the supported subset. These rules match the design used throughout this tutorial: every field is present, and missing information is represented by null (Lesson 22).
Always: Semantic Validation A schema confirms that duration_weeks is an integer. It does not confirm that the integer is right. Semantic validation checks values against the source and your business rules (Lesson 73):
def check_course(course: CourseInfo, description: str) -> list[str]: issues = [] if course.title.lower() not in description.lower(): issues.append("title does not appear in the description") if course.duration_weeks is not None and not 1 <= course.duration_weeks <= 52: issues.append("duration_weeks is outside the plausible range 1-52") if course.duration_weeks is not None and str(course.duration_weeks) not in description: issues.append("duration_weeks value does not appear in the description") return issues Code Explanation Title grounding: the extracted title must appear in the description, as the prompt required. Range check: a 300-week course is structurally valid but almost certainly wrong. Number grounding: the duration should appear as a number in the source. This simple check can produce false alarms for descriptions that write "six weeks" in words; treat such issues as warnings for review. Designing Schemas That Work Well Guideline Reason Use descriptive field names, such as duration_weeks The name tells the model the unit Add descriptions to fields They act as instructions inside the schema Use enums or Literal for categories Prevent variations such as "beginner" and "Beginner level" Use nullable fields for optional information Avoid invented values Keep nesting shallow Deeply nested structures are harder to fill correctly and to validate Include a reason or evidence field when useful Supports auditing and debugging (Lesson 84, Recipe 6) Version your schemas Changing fields is a breaking change for consuming code (Lesson 67) Order Can Matter Models generate fields in order. If a field such as reason or evidence should influence a decision, place it before the decision field, so the decision is generated after the supporting text, similar to reasoning before answering (Lesson 39).
Choosing a Level Situation Recommended Approach Production feature using a model that supports Structured Outputs Level 3 + semantic validation Model or API without schema support Level 1 with validation and retry, or Level 2 + validation Quick prototype Level 1 with basic parsing Tool or function arguments Native tool calling with strict schemas (Lesson 78) Common Mistakes Mistake 1: Trusting Prompt-Only JSON Without Validation What it is: Calling json.loads() and using the result directly.
Why it happens: The format works in testing.
What it causes: Crashes or wrong data when a key is missing or a type changes.
How to fix it: Validate with a schema model, and retry with specific feedback.
Mistake 2: Believing a Schema Guarantees Correct Values What it is: Skipping checks because Structured Outputs is enabled.
Why it happens: "Guaranteed schema adherence" sounds like guaranteed correctness.
What it causes: Wrong but well-formatted values in your database.
How to fix it: Add semantic validation for values that matter.
Mistake 3: Optional Fields Without Null What it is: Leaving optional fields out of required in strict schemas, or forcing every field to have a value.
Why it happens: Different conventions for optional data.
What it causes: Schema errors in strict mode, or invented values when information is missing.
How to fix it: Make every field required, with nullable types for optional information.
Mistake 4: Not Handling Missing Parsed Output What it is: Assuming output_parsed always contains data.
Why it happens: It works in normal cases.
What it causes: Errors when the model declines or no structured result is returned.
How to fix it: Check for None and handle it explicitly.
Mistake 5: Changing Schemas Without Versioning What it is: Renaming or removing fields in place.
Why it happens: Small schema changes seem harmless.
What it causes: Code that reads the old field names breaks.
How to fix it: Version schemas and update consumers deliberately.
Best Practices Choose the strongest available level of guarantee. Structured Outputs where supported.
Define schemas in code. Pydantic models give both schemas and validation.
Make every field required, with nullable types for optional values. Missing information becomes null, never invented.
Keep content rules in the instructions. The schema handles format; the prompt handles meaning.
Validate semantics in code. Grounding, ranges, and business rules.
Retry with specific feedback, and handle failure. Limited attempts, then escalation.
Try It Yourself Exercise 1: Compare Levels Write five course descriptions, including one that omits the level and one that writes the duration in words. Extract them with Level 1 (prompt-only with validation) and Level 3 (responses.parse). Compare how often each produced valid structured data on the first attempt.
Exercise 2: Semantic Checks Run check_course() on all results from Exercise 1. Identify which issues are real errors and which are false alarms, such as durations written in words, and improve the check to handle "six weeks".
Exercise 3: Design a Schema Design a Pydantic model for extracting support ticket details: category (enum), urgency (enum), course name (nullable), order ID (nullable), and a short summary. Add a reason field placed before the urgency field. Use it with responses.parse on ten tickets and evaluate the results.