.NETAI AgentsStructured OutputJSON

Structured Output from .NET Agents: Getting Reliable JSON in C#

An agent that returns prose is hard to program against. How to get reliable, strongly-typed JSON out of a .NET agent — with schemas, typed responses, and validation.

Prose is great for a chat window and terrible for a program. The moment you want an agent’s output to drive code — populate a form, branch on a category, call another service — you need structured output: reliable, parseable, ideally strongly-typed data instead of a paragraph. “Please respond in JSON” in the prompt is not reliable enough for production. This article covers the techniques that actually are, in C#.

If you’re new to agents, start with the tool-using agent tutorial.

Why prompting for JSON isn’t enough

Ask a model to “return JSON” and most of the time it will — but “most of the time” is a bug generator. It occasionally wraps the JSON in markdown fences, adds a chatty preamble (“Sure! Here’s the JSON:”), or emits a trailing comma. Your JsonSerializer.Deserialize then throws in production, at 2 a.m., on an input you didn’t test. You need a mechanism that constrains the output, not a polite request.

Technique 1: Ask for a typed result directly

The cleanest option is to let the framework do the work. Microsoft.Extensions.AI supports requesting a strongly-typed response: you give it your C# type, it generates a JSON schema from that type, instructs the model to conform, and deserializes the result back into your object. Conceptually:

public record SupportTicket(
    string Category,      // e.g. "billing", "technical", "account"
    string Priority,      // "low" | "medium" | "high"
    string Summary);

var result = await chatClient.GetResponseAsync<SupportTicket>(
    "Classify this message: 'I was charged twice this month and I'm furious.'");

SupportTicket ticket = result.Result;   // a real C# object, not a string
Console.WriteLine($"{ticket.Category} / {ticket.Priority}");

You get a SupportTicket you can use immediately — no manual parsing, no fence-stripping. Under the hood it uses the provider’s structured-output / JSON-schema support, so the model is constrained to your shape rather than politely asked.

Technique 2: Provider-enforced JSON schema

When you need a raw JSON object rather than a mapped type, most providers now support a response format that enforces a JSON schema at the API level — the model literally cannot return non-conforming output. Set the response format to your schema and the provider guarantees valid, schema-matching JSON. This is stronger than prompt instructions because the constraint lives in the decoding, not the prompt.

Technique 3: Function calling as structured output

There’s a neat trick: a tool’s parameters are a schema. If you define a tool whose arguments match the structure you want and force the model to call it, the model’s tool-call arguments are your structured data — validated against the tool’s schema. This works well on providers with solid function-calling but weaker dedicated JSON modes, and it composes naturally if you’re already using tools.

Always validate — constraints reduce, don’t eliminate, risk

Even with schema enforcement, treat the output as untrusted until you’ve checked it. Schema-valid JSON can still be semantically wrong — a Priority of "urgent" when your enum only allows low/medium/high, or a date in the wrong format.

if (!Enum.TryParse<Priority>(ticket.Priority, ignoreCase: true, out var p))
{
    // Retry once with the error fed back, or fall back to a default.
}

A good pattern is validate, and on failure retry once with the validation error appended to the prompt — models are usually good at fixing a mistake they’re told about. Cap the retries so a stubborn failure doesn’t loop forever (and cost you — see cost control).

Keep schemas small and specific

The model conforms better to a tight schema than a sprawling one. Prefer enums over free-text where you can (Category as a fixed set, not any string), keep nesting shallow, and describe each field. A focused schema is both more reliable and cheaper, since the description tokens are smaller.

Note: exact method names (GetResponseAsync<T>) and response-format APIs are still stabilizing across Microsoft.Extensions.AI and provider clients. Verify against the current .NET AI docs; the techniques — typed responses, provider-enforced schemas, function-calling-as-schema, validate-and-retry — are the durable part.

Takeaway

Don’t ask an agent for JSON and hope. Constrain it: request a strongly-typed result and let the framework generate the schema and deserialize, use provider-enforced JSON schemas when you need raw objects, or exploit function-calling as a structured-output channel. Then validate the result semantically and retry once on failure. That’s the difference between an agent whose output you can read and one whose output your code can depend on.


Have a correction or a topic you want covered? Email mani.bc72@gmail.com.