The Problem With Parsing Free Text

Ma Mahalakshmi V Updated 16 Sep 2026
16 min read ·Lesson 21 of 224

Why This Problem Exists At All

Every response you've generated so far in this course has come back as response.output_text — a plain string. That's fine when a human is going to read the result. It becomes a real engineering problem the moment your application needs to do something programmatic with the model's answer: store a specific field in a database, populate a form, trigger a different code path depending on a category the model assigned, or pass a number into a calculation. At that point, a string of natural language is not actually the data your code needs — it's a document your code has to first turn into data, and that extra step is where a surprising amount of fragility creeps into LLM-backed applications.

response = client.responses.create(
    model="gpt-5.6-luna",
    input="Extract the name, age, and city from: 'Maria Gomez, 34, lives in Austin.'",
)
print(response.output_text)
# Something like: "Name: Maria Gomez\nAge: 34\nCity: Austin"

This output is perfectly readable to a person. To a program, it's an unstructured blob that has to be picked apart with string operations before anything useful can be done with it — and that picking-apart step is exactly what this lesson is about, and exactly what the rest of this unit shows you how to avoid needing in the first place.

The Naive Approach: Regular Expressions and String Splitting

The most direct way to turn the text above into usable data is to write parsing code against whatever format the model happened to produce.

import re

def parse_naive(text: str) -> dict:
    name_match = re.search(r"Name:\s*(.+)", text)
    age_match = re.search(r"Age:\s*(\d+)", text)
    city_match = re.search(r"City:\s*(.+)", text)
    return {
        "name": name_match.group(1).strip() if name_match else None,
        "age": int(age_match.group(1)) if age_match else None,
        "city": city_match.group(1).strip() if city_match else None,
    }

result = parse_naive(response.output_text)
print(result)  # {'name': 'Maria Gomez', 'age': 34, 'city': 'Austin'}

This works, for this exact output, on this exact day, with this exact model and prompt. That last clause is the entire problem with this approach: it works only under the assumption that the model will always format its answer exactly the same way, every single time, forever. That assumption does not hold, for reasons that are worth understanding precisely rather than taking on faith.

Why the Model's Formatting Is Not Reliable Enough to Parse

A language model generates text one token at a time, and even with an identical prompt, its output is not guaranteed to be byte-for-byte identical across calls (Unit 3 touched on this when discussing example-driven prompting to steer output shape). A model might, on one call, answer with "Name: Maria Gomez", and on another, equally reasonable-seeming call, answer with "The name is Maria Gomez", or "**Name**: Maria Gomez", or format age as "34 years old" instead of a bare number, or omit a field the parsing code assumes will always be present, or reorder the fields, or add a sentence of preamble before the structured-looking part even begins.

# Two calls with the identical prompt can produce meaningfully different shapes
response_a = client.responses.create(model="gpt-5.6-luna", input=extraction_prompt)
response_b = client.responses.create(model="gpt-5.6-luna", input=extraction_prompt)

print(response_a.output_text)
# "Name: Maria Gomez\nAge: 34\nCity: Austin"

print(response_b.output_text)
# "Here's what I found:\n- Name: Maria Gomez\n- Age: 34 years old\n- City: Austin, TX"

Notice the second response adds a leading sentence, switches to bullet points, appends "years old" to the age (breaking the int() conversion the naive parser above depends on), and appends a state abbreviation to the city. None of this is the model "misbehaving" — every one of these responses is a reasonable, human-readable answer to the prompt as asked. The problem is entirely on the parsing side: brittle, format-assuming parsing code has no way to tell a superficial formatting variation from an actual change in the underlying data, and breaks on the former just as readily as it would need to react to the latter.

The Categories of Failure This Creates

It's worth naming the specific ways free-text parsing fails in production, since each has a slightly different signature and a different level of danger.

Silent wrong extraction. The regex matches something, but not the right thing — for instance, a name containing the substring "City" derails a naive city-matching pattern, or a stray digit elsewhere in the text gets picked up as the age instead of the real one. This is the most dangerous failure mode because nothing crashes and no error is raised; the application simply proceeds with subtly incorrect data, and the bug may not surface until someone notices the wrong value much later, possibly after it has already propagated into a database, a report, or a decision made from it.

Loud parsing failure. A regex simply fails to match, age_match is None, and int(age_match.group(1)) raises an AttributeError because NoneType has no .group() method. This is more annoying than the silent case, but strictly safer — it's immediately visible that something went wrong, rather than quietly propagating bad data downstream.

Partial extraction. Some fields parse correctly and others don't, producing a record that is neither cleanly successful nor cleanly failed — often the hardest case to handle gracefully, since "half of this record is trustworthy" doesn't map cleanly onto typical error-handling patterns that expect a clean success/failure boundary.

def demonstrate_failure_modes():
    # Silent wrong extraction: this "city" pattern greedily grabs too much
    text_1 = "City: Austin, and the person also mentioned City Hall as a landmark."
    match = re.search(r"City:\s*(.+)", text_1)
    print(match.group(1))
    # "Austin, and the person also mentioned City Hall as a landmark."
    # Wrong, but no exception is raised — this is the dangerous, silent case.

    # Loud parsing failure: no "Age:" label present at all
    text_2 = "Maria Gomez is 34 and lives in Austin."
    match = re.search(r"Age:\s*(\d+)", text_2)
    print(match)  # None — and code assuming a match will crash immediately after

demonstrate_failure_modes()

Why "Just Improve the Prompt" Only Gets You So Far

A natural response to this problem is to make the prompt more explicit about the desired format: "Respond ONLY in the exact format: Name: , Age: , City: , with no other text." This genuinely helps — it's a real, valid technique, and Unit 3's discussion of instructions and few-shot examples applies directly here — but it does not fully solve the underlying problem, for two reasons worth being explicit about.

First, it's a request, not a constraint. The model is still generating free-form text token by token; a carefully worded instruction increases the probability that the output matches the requested format, but does not make it structurally impossible for the model to deviate, especially on edge cases, unusual inputs, or longer conversations where earlier instructions carry proportionally less weight than they did at the start (a dynamic Unit 4's discussion of context and compaction touches on from a different angle).

response = client.responses.create(
    model="gpt-5.6-luna",
    instructions="Respond ONLY in the exact format: Name: <name>, Age: <number>, City: <city>. No other text.",
    input="Extract the name, age, and city from: 'The person mentioned they are unsure of their exact age, roughly mid-30s, and recently moved from Austin to Denver.'",
)
print(response.output_text)
# The model may reasonably produce something like:
# "Name: [not stated], Age: mid-30s (approximate), City: Denver (recently moved from Austin)"

This response is arguably a better, more honest answer to a genuinely ambiguous input than a rigidly formatted but misleading one would be — but it also completely breaks any parser expecting a clean integer for age and a single unambiguous city string. The model correctly recognized real ambiguity in the input; the prompt's formatting instruction didn't anticipate that ambiguity and had no mechanism to force a structurally valid answer despite it.

Second, even when the model does follow the requested format faithfully, "an instruction the model usually follows" and "a format the code can structurally rely on" are different guarantees, and production software generally needs the latter. A parsing approach that works 95% of the time and fails unpredictably the other 5% is often worse than a mechanism that is structurally guaranteed to work 100% of the time in a known way — because the 95% case creates a false sense of reliability that the 5% case then violates unpredictably, often in production, often at the worst possible moment.

What "Structured" Actually Means in This Context

The term "structured output," which the rest of this unit builds toward, refers to a fundamentally different approach: rather than asking the model to produce text that looks like structured data and hoping the format holds, you give the API a formal schema — a precise specification of exactly which fields must exist, what type each one must be, and what values are and aren't valid — and the generation process itself is constrained so that its output is guaranteed, by construction, to conform to that schema. This is not a stronger version of prompt-engineering the desired format; it's a structurally different mechanism operating at a different layer, which Lesson 2 of this unit explains in full technical detail.

# A preview of where this unit is headed — full mechanics in Lesson 2
response = client.responses.create(
    model="gpt-5.6-luna",
    input="Extract the name, age, and city from: 'Maria Gomez, 34, lives in Austin.'",
    text={
        "format": {
            "type": "json_schema",
            "name": "person_extraction",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "city": {"type": "string"},
                },
                "required": ["name", "age", "city"],
                "additionalProperties": False,
            },
            "strict": True,
        }
    },
)
import json
data = json.loads(response.output_text)
print(data)  # {'name': 'Maria Gomez', 'age': 34, 'city': 'Austin'}

The critical difference from everything shown earlier in this lesson: data["age"] is guaranteed to be present and guaranteed to be a JSON number, not a string that might or might not contain "years old" appended to it, not a field that might or might not be present depending on how the model chose to phrase its answer. This guarantee comes from the schema being enforced during generation itself, not from a hopeful instruction asking the model to please format things a certain way.

A Concrete Comparison of the Two Approaches

AspectFree-text + parsingStructured output (schema-enforced)
Format guaranteeNone — depends on the model happening to follow a requested formatEnforced during generation — output is guaranteed valid per schema
Parsing code neededCustom regex/string logic per use case, brittle to phrasing changesA single, generic json.loads() call plus your schema definition
Failure modeSilent wrong extraction or crashes on unexpected phrasingField types and presence guaranteed; only content correctness varies
Handles ambiguous inputMay produce free-form hedging text that breaks rigid parsingMust still produce a schema-valid value — Lesson 4 covers designing schemas for genuinely ambiguous or missing data
Maintenance burdenGrows with every new field or edge case encountered in productionConcentrated in one schema definition, versioned and reviewed like code

This table is the argument this lesson has built toward: free-text parsing asks the model to hope its formatting stays consistent and asks your code to hope it correctly guesses that formatting; structured outputs remove both of those hopes and replace them with an explicit, enforced contract between your application and the model.

When Free-Text Output Is Still the Right Choice

It's worth being precise about scope: nothing in this lesson means free-text output is always wrong. When a response is meant to be read by a person — a chat reply, a written explanation, a long-form document — free text is exactly the right shape, and wrapping it in a rigid schema would be pointless overhead solving a problem that doesn't exist in that context. Structured outputs matter specifically when the consumer of the response is code: a database write, a conditional branch, a downstream API call, a form population. The test worth applying to any given response is simple: will a program read this output, or will a person? If a program will read it, structured outputs (covered starting in Lesson 2) are very likely the right tool; if a person will read it, plain text remains the right choice, and no schema is needed at all.

A Common Intermediate Step: Asking the Model to Produce JSON as Text

Before structured outputs existed as a first-class feature, a common workaround was asking the model to produce JSON-formatted text as its answer, then parsing that text with json.loads() — an improvement over ad hoc regex parsing, but still fundamentally a free-text approach with all of this lesson's failure modes intact, just wearing JSON's syntax as a costume rather than solving the underlying reliability problem.

response = client.responses.create(
    model="gpt-5.6-luna",
    instructions="Respond with a JSON object with keys name, age, and city. Output only the JSON, nothing else.",
    input="Extract the name, age, and city from: 'Maria Gomez, 34, lives in Austin.'",
)

import json

try:
    data = json.loads(response.output_text)
except json.JSONDecodeError as e:
    print(f"Failed to parse as JSON: {e}")
    data = None

This is meaningfully better than the regex approach earlier in this lesson — json.loads() at least enforces that the overall structure is syntactically valid JSON, rather than relying on hand-written pattern matching. But it is still an unenforced request: nothing prevents the model from wrapping the JSON in a markdown code fence ( json ... ), adding a sentence of commentary before or after it, using a different key name than requested ("full_name" instead of "name"), omitting a field it considers uncertain, or nesting the requested keys inside an extra wrapper object. Each of these is a plausible, reasonable-looking response that nonetheless breaks a parser assuming an exact key structure.

# A plausible response that breaks naive JSON parsing despite being "basically JSON"
raw = '```json\n{"name": "Maria Gomez", "age": 34, "city": "Austin"}\n```'
try:
    data = json.loads(raw)  # raises JSONDecodeError — the code fence isn't valid JSON syntax
except json.JSONDecodeError:
    # A common defensive workaround: strip markdown fencing before parsing
    cleaned = raw.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
    data = json.loads(cleaned)
    print(data)

Needing this kind of defensive string-stripping before you can even attempt to parse JSON is itself a symptom of the same underlying issue this lesson has been describing: the format is still fundamentally a request, honored by convention rather than guaranteed by construction, and every convention the model might reasonably follow (or fail to follow) becomes another edge case your parsing code has to anticipate.

A Worked Example With Nested and Optional Data

The failure modes above become sharper once the data being extracted has any real structure to it — a list of items, an optional field, a nested object — rather than three flat, always-present string and number fields. Consider extracting a structured order summary from a free-form customer message.

message = (
    "Hi, I'd like to order 2 large coffees and a blueberry muffin. "
    "Actually, could you also throw in a bottle of water if you have any? "
    "I'll pick it up around 3pm."
)

response = client.responses.create(
    model="gpt-5.6-luna",
    instructions="Extract the order as a list of items with quantities, and the pickup time if mentioned.",
    input=message,
)
print(response.output_text)

A response to this prompt could reasonably take any of several shapes: a numbered list, a sentence describing the items, a table-like block of text, with the conditional "if you have any" water potentially included or omitted depending on how the model weighs its conditional phrasing, and the pickup time expressed as "3pm", "15:00", or "around 3:00 PM". Writing parsing code robust to every one of these variations — for a data shape that isn't even particularly complex — starts to require substantially more defensive logic than the three-field example earlier in this lesson, and the complexity only grows from there for genuinely nested data (an order with per-item modifiers, a multi-address shipment, a form with repeating sections). This scaling problem is a large part of why structured outputs, covered starting in the next lesson, matter more as extraction tasks grow more realistic and complex — hand-written parsing logic that's merely annoying for three flat fields becomes genuinely unmanageable for realistically nested, partially-optional data.

Why This Matters More as Volume Increases

A parsing approach that fails on roughly one request in a hundred might seem tolerable when testing by hand, one request at a time — but production applications rarely process one request at a time. An extraction pipeline processing ten thousand customer messages a day, at even a modest 1% silent-failure rate, produces on the order of a hundred incorrectly extracted records daily, most of which nobody manually reviews before they reach a database, a report, or a decision made from them. This is precisely the scenario structured outputs are built to prevent: not by making the model smarter about the specific extraction task, but by making the format of its answer impossible to get structurally wrong, which shrinks the space of possible failures down to genuine content-level mistakes (extracting the wrong value into a correctly-typed field) rather than format-level ones (a field missing, wrongly typed, or wrapped in unexpected text) — a distinction Lesson 2 develops further once the schema mechanism itself is on the table.

Common Mistakes

Writing parsing code against one observed example of the model's output, rather than testing against several calls, several rephrasings of the prompt, and several edge-case inputs, and discovering only in production that the format isn't as consistent as the single example suggested.

Treating a formatting instruction in the prompt as equivalent to a structural guarantee, and skipping validation of the parsed result entirely, on the assumption that a clearly worded instruction is sufficient on its own.

Using free-text parsing for a response with an obvious, learnable structure (a fixed set of fields, known types) when a schema-based structured output, covered in the rest of this unit, would eliminate the parsing problem at its root rather than requiring ever more defensive parsing code to paper over it.

Not distinguishing between silent wrong extraction and loud parsing failure when designing error handling — code that only checks "did this raise an exception" misses the more dangerous case where a regex matched something plausible-looking but incorrect.

Best Practices

Ask, for any given response, whether a person or a program is the actual consumer of that output, and let the answer decide between free text and a structured, schema-enforced format rather than defaulting to one or the other out of habit.

Treat a prompt-level formatting instruction as a helpful nudge, never as a substitute for an enforced schema, when the downstream code genuinely needs a guarantee rather than a strong tendency.

Prefer raising a clear, immediate error over a parser that silently extracts a plausible-but-wrong value, when structured outputs aren't yet in place and free-text parsing is still what's driving an extraction task.

Treat the rest of this unit's schema-based approach as the default for any new feature that extracts, classifies, or otherwise turns model output into program-consumed data, reserving hand-rolled text parsing for the shrinking set of cases where a schema genuinely isn't practical to define.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about The Problem With Parsing Free Text and get answers drawn from it.

Signed-in readers only.