Skip to main content

Command Palette

Search for a command to run...

Model Returns Invalid JSON in .NET: Structured Output Fixes

Updated
10 min read
Model Returns Invalid JSON in .NET: Structured Output Fixes

Structured output is one of the most useful capabilities in Microsoft.Extensions.AI - calling GetResponseAsync<T>() and getting a typed C# object back instead of raw text. In production, I've seen teams ship it confidently in development and then hit a wall when it fails silently or throws a JsonException in staging. The model returns something that is almost JSON, or JSON with a missing field, or worst of all - a completely valid-looking response that does not match the schema at runtime.

The full working implementation, including validation, retry logic, and edge case handling, is available on Patreon - with annotated, production-ready code that maps directly to what you would ship in a real AI-powered endpoint. Most tutorials stop at the happy path; the complete version has all the failure modes handled.

The pattern here maps directly to Chapter 6 of the AI-Powered .NET APIs course, which covers structured outputs inside a full ASP.NET Core support-desk API - classification, extraction, and triage, all returning typed C# objects reliably. Wiring it into a running API with real provider configuration is where the concept clicks.

AI-Powered .NET APIs

What Does "Invalid JSON from Structured Output" Actually Mean?

When you call GetResponseAsync<T>() - or the more explicit path via ChatResponseFormat.ForType<T>() - Microsoft.Extensions.AI asks the underlying model to return JSON that conforms to a generated schema. When this fails, you will see one of several symptoms:

  • A JsonException thrown during deserialization

  • A null result where you expected a populated object

  • A correctly shaped object with empty or default fields that should have values

  • A provider-level error, particularly with strict-schema models on OpenAI or Azure OpenAI

Each symptom maps to a distinct root cause. Here are the six I have seen consistently across both local Ollama routes and cloud provider routes in production.

Cause 1: The Model Does Not Support Native JSON Schema Enforcement

GetResponseAsync<T>() uses useNativeJsonSchema: true by default. This instructs the provider to enforce the JSON schema at the model level - the reliable path when the model supports it. But not every model does. Smaller local models via Ollama and some older cloud models will either silently ignore the schema constraint or fail with an opaque provider error.

The quick diagnostic: call GetResponseAsync<string>() on the same prompt and inspect the raw output. If the model is returning prose with embedded JSON, or JSON wrapped in markdown code fences, native schema enforcement is not being applied.

The fix: pass useNativeJsonSchema: false for models that do not support it, and use system prompt instructions to guide the expected shape. Then validate the result manually before consuming it.

// Use false for models that cannot enforce a native schema
var result = await chatClient.GetResponseAsync<TicketClassification>(
    prompt,
    useNativeJsonSchema: false
);

In practice I have seen teams run true for cloud model routes and false for Ollama-backed local routes, switching via configuration with a one-line provider swap - exactly the pattern Microsoft.Extensions.AI is designed for. The official quickstart on structured output covers provider configuration in more detail.

Cause 2: Your Output Type Contains JSON Schema-Incompatible Properties

This is the cause I hit most often. Developers model output types naturally in C#, without considering what maps cleanly to a JSON Schema object. When Microsoft.Extensions.AI generates the schema from the type, it encounters types it cannot express precisely, and the model either guesses incorrectly or returns a structure that fails deserialization.

Types that commonly cause this:

  • DateTime and DateTimeOffset - not JSON Schema primitives

  • Uri - not a JSON Schema primitive

  • Guid - works on some providers, fails silently on others

  • Custom strongly-typed IDs or value objects without explicit schema guidance

The fix is to use JSON Schema-safe types and add [Description] attributes to give the model explicit formatting instructions:

// Problematic - DateTime is not a JSON Schema primitive
public class ReviewResult
{
    public string Title { get; set; } = string.Empty;
    public DateTime ReviewedAt { get; set; }
}

// Fixed - use string, parse and validate after deserialization
public class ReviewResult
{
    public string Title { get; set; } = string.Empty;
    [System.ComponentModel.Description("ISO 8601 date string, e.g. 2026-06-22T10:00:00Z")]
    public string ReviewedAt { get; set; } = string.Empty;
}

Always parse and validate after deserialization. Treat model output as untrusted external input - the model could still return a string that is not a valid date.

Cause 3: The System Prompt Does Not Reinforce the Expected Output Format

Even with native schema enforcement, models with weaker instruction-following capability benefit significantly from an explicit format instruction in the system prompt. If your system prompt is generic or says nothing about output format, some models will prepend an explanation before the JSON block or wrap it in markdown.

In production I have seen this exact failure sequence: the model returns "Here is the classification: { ... }" - the JSON is valid and correct, but the leading text breaks deserialization. The data was there; the wrapper was the problem.

The fix: explicitly instruct the model in the system prompt:

"Respond with a JSON object only. Do not add explanations, comments, or markdown code fences."

This is particularly important for Ollama-hosted models and any model below GPT-4-class instruction following. For prompt management best practices within Microsoft.Extensions.AI, Chapter 4 of the course covers system prompts as versioned application assets.

Cause 4: Context Window Overflow Truncates the JSON

If you are feeding long documents into a retrieval-augmented generation pipeline (see The RAG Pattern in ASP.NET Core) alongside a complex output schema, the model may exhaust its token budget before completing the JSON object. The result is truncated JSON - syntactically invalid, with the last several fields missing and no closing brace.

This is a silent failure. No exception comes from the provider. The incomplete JSON string is returned and deserialization fails at your end.

Diagnostics: log the raw ChatResponse.Message.Text before deserialization. If you see truncated JSON, check the combined token count of your prompt plus expected response size against the model context window limit.

The fix: reduce prompt size through more aggressive document chunking, trim the conversation history, or route structured extraction tasks to a model with a larger context window.

Cause 5: Schema Differences Across Environments

This one is subtle and takes time to diagnose. Microsoft.Extensions.AI generates the JSON schema from your C# type using reflection and AIJsonUtilities.CreateJsonSchema(). If your output type has different nullability annotations between builds, or if you are running different Microsoft.Extensions.AI NuGet versions across environments, the generated schema will differ.

The model in development is told one schema; the model in staging is told a different one. The responses match the schema each environment presented - but your runtime type does not match what the staging model was responding to.

The fix: pin Microsoft.Extensions.AI and Microsoft.Extensions.AI.Abstractions versions explicitly across all project files. Log the generated schema string during startup in non-production environments - AIJsonUtilities.CreateJsonSchema(typeof(YourType)).ToString() - so you can diff it across deploys and catch drift before you ship.

Cause 6: Trusting Deserialized Output Without Validation

This is not a JSON parsing failure in the strict sense, but it is the one that creates the most production incidents. A fully deserialized structured output should be treated as untrusted input from an external system. The model may:

  • Set required string fields to "" or "N/A" instead of meaningful values

  • Return numeric values outside valid ranges (a confidence score of 1.5 when 0.0-1.0 is expected)

  • Populate all fields correctly but with semantically wrong values - the model hallucinated the classification

In one support API I worked on, a ticket triage model would reliably return a syntactically valid TicketCategory value that was the wrong category for ambiguous inputs. Deserialization succeeded. The downstream routing logic acted on bad data.

The fix: validate deserialized output before it reaches any downstream service or business logic. FluentValidation works well here - you already know the expected constraints on your output type. For a broader perspective on treating model output as unsafe input, the Preventing Prompt Injection in ASP.NET Core AI APIs post covers this alongside input filtering patterns.

How to Avoid Structured Output Issues in Production

A short checklist that covers the most common scenarios:

  1. Verify model support - test with a simple type before using useNativeJsonSchema: true in production

  2. Use JSON Schema-safe types - string for dates and URIs, parse and validate after deserialization

  3. Include explicit format instructions in the system prompt, even when using native enforcement

  4. Log raw model output during development and staging - not just the deserialized result

  5. Validate deserialized output before passing it to any downstream service

  6. Monitor token usage per endpoint to detect context overflow before it becomes a production failure

  7. Pin NuGet versions for Microsoft.Extensions.AI across all environments and audit schema changes on every update

For a deeper look at how IChatClient and structured outputs fit into ASP.NET Core DI and provider configuration, the Microsoft.Extensions.AI and IChatClient overview post covers the foundational setup in detail.

Frequently Asked Questions

Can I use GetResponseAsync<T>() with all Ollama models?

Not reliably. Models like llama3.2, phi4, and qwen2.5 vary in their structured output support. Always test with useNativeJsonSchema: true first. If the model returns malformed or freeform output, switch to useNativeJsonSchema: false and use prompt instructions to guide the format. Validate the result in both cases.

Does useNativeJsonSchema: true guarantee valid JSON on every response?

On providers that enforce it at the model level - OpenAI with strict: true, Azure OpenAI structured outputs - yes, deserialization is guaranteed to succeed if no exception is thrown. On providers where it is simulated via prompt guidance only, reliability depends on the model's instruction-following capability.

What happens if the model returns the correct shape but wrong values?

Deserialization succeeds but your application logic receives semantically bad data. This is not caught by schema enforcement - it requires validation logic on the deserialized object. Use FluentValidation or custom guards to check value ranges, required fields, and enum validity before acting on the result.

Should I retry on a structured output deserialization failure?

Yes, with limits. A single corrupted response often succeeds on retry. A good production pattern: retry once with the same prompt, then retry again with an explicit system prompt reminder requesting clean JSON only. After two retries, log the failure and return a safe default or error response to the caller. Do not retry indefinitely - it burns tokens and adds latency.

How do I see exactly what JSON schema is being sent to the model?

Call AIJsonUtilities.CreateJsonSchema(typeof(YourOutputType)).ToString() in a test or during startup logging. This makes the generated schema visible and lets you compare it across environments or after a NuGet update.

Does this apply to structured outputs via Microsoft Agent Framework as well?

The same root causes apply - unsupported types, context overflow, missing format instructions - but the configuration path differs. With Microsoft.Agents.AI, structured output is typically handled by a decorator agent that post-processes the text response into a typed object. The validation and schema-safety principles are identical regardless of whether you are using IChatClient directly or through the Agent Framework.


About the Author

I'm Celin Daniel, Co-founder of Coding Droplets. I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.

More from this blog

C

Coding Droplets

268 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.