# 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](https://www.patreon.com/CodingDroplets) - 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](https://aiapis.codingdroplets.com/), 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](https://newsletter.codingdroplets.com/images/ai-api-course-banner-1.jpg align="center")](https://aiapis.codingdroplets.com/)

## 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.

```csharp
// 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](https://learn.microsoft.com/en-us/dotnet/ai/quickstarts/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:

```csharp
// 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](https://codingdroplets.com/rag-pattern-aspnet-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](https://codingdroplets.com/preventing-prompt-injection-aspnet-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](https://codingdroplets.com/microsoft-extensions-ai-ichatclient-aspnet-core-enterprise-2026) 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](https://codingdroplets.com/). 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.

*   GitHub: [codingdroplets](http://github.com/codingdroplets/)
    
*   YouTube: [Coding Droplets](https://www.youtube.com/@CodingDroplets)
    
*   Website: [codingdroplets.com](https://codingdroplets.com/)
