Getting Reliable JSON Out of LLMs in C#: Structured Output That Doesn't Break
Chatbots are the demo; data extraction is the workhorse. Most of the LLM value I've shipped in real systems looks nothing like chat: parse this emailed purchase order into line items, classify this support ticket, extract entities from this contract, turn this free-text address into fields. All of these need the model to return data your code can deserialize — and the naive approach ("please respond in JSON") produces the flakiest integration you'll ever maintain: markdown-fenced JSON, trailing commentary ("Here's your JSON!"), invented fields, missing ones.
Modern APIs solve this properly. Here's the reliable pattern in C#.
Structured Outputs: schema-enforced JSON
OpenAI's Structured Outputs (and equivalents elsewhere) constrain the model's generation so the response must conform to a JSON schema you supply — not "usually", but grammatically enforced during token generation:
With a matching C# record:
Notes that save debugging time:
strict: true+additionalProperties: false+ everything inrequiredis the combination that gets you the hard guarantee. Strict mode demands it.- Enums are your best friend.
"category": "billing"from a closed set beats free text you'd have to normalize. Push every classification field into an enum. - The shape is guaranteed — the content is still a model's judgment.
"priority": "urgent"will always be a valid enum value; whether it's the right call is a quality question (that's what evaluation sets are for).
Keeping schema and C# types in sync
Hand-writing schemas that mirror your records is a drift bug waiting to happen. Generate the schema from the type — .NET has this built in now:
Or skip the plumbing entirely with Microsoft.Extensions.AI, which turns the whole pattern into one generic call:
One generic parameter: schema generated from the record, sent as structured output, response deserialized. This is what I use in application code today.
The real-world example: document extraction
Here's the shape of the feature that pays the bills — pulling structured line items from messy human text:
Design lessons learned doing this on real documents:
- Make uncertainty representable. Nullable fields plus the instruction "use null for anything not present" gives the model an honest out. A schema with only required non-null fields forces hallucination when data is missing — the model must put something there.
- Validate business rules after deserialization. Schema enforcement guarantees types, not sense: quantity −3 or a delivery date in 1987 can still arrive. Run the result through the same validation you'd apply to any API input (FluentValidation slots in perfectly).
- Keep a confidence escape hatch. For high-stakes extraction, add an
"extractionIssues": string[]field to the schema and prompt the model to note anything ambiguous — then route non-empty ones to human review. This one field turned a risky automation into an accepted one at a previous employer.
The pipeline that makes it production-grade
Text in → typed object out is only the happy path. Wrap it:
- Deserialize (should never fail with strict mode — but guard anyway; treat failure as a retryable error).
- Validate business rules; reject or route to review on failure.
- Log input, output, token usage, and validation verdicts — you'll need this history the first time someone asks "why did it extract that?"
- Evaluate on a growing test set of real inputs with known-correct outputs. When you change the prompt, schema, or model, the test set tells you whether you got better or worse. Prompt changes are code changes; treat them with the same seriousness.
The takeaway
Stop parsing model prose. With schema-enforced structured output, an LLM becomes a well-typed function: string → TicketTriage, string → PurchaseOrder — a component you can compose, validate, and test like anything else in your codebase. It's the single most production-ready pattern in applied AI right now, and C# is genuinely one of the best languages to do it in.
Where to go next
- LLM Function Calling in C# — the same schema discipline, applied to actions instead of data.
- C# Records vs Classes vs Structs — why records are the perfect target type for extraction.
- RAG Explained for .NET Developers — combine retrieval with structured answers.