Skip to main content

Azure OpenAI Structured Outputs in C#: JSON Schema Guide

Intermediate Original .NET 9 Azure.AI.OpenAI 2.1.0 OpenAI 2.1.0 Microsoft.SemanticKernel 1.54.0
By Rajesh Mishra · Mar 21, 2026 · 13 min read
Verified Mar 2026 .NET 9 Azure.AI.OpenAI 2.1.0
In 30 Seconds

Azure OpenAI structured outputs guarantee 100% JSON schema compliance from GPT-4o and GPT-4o-mini when using API version 2024-08-01-preview or later. In the OpenAI .NET SDK, use ChatResponseFormat.CreateJsonSchemaFormat<T>(name, strict: true) in ChatCompletionOptions. Semantic Kernel supports structured outputs via ResponseFormat = typeof(T) in OpenAIPromptExecutionSettings. Schema types must use only JSON Schema primitives — string, int, bool, decimal, enums, nested objects, and arrays. Replace DateTime, Uri, and TimeSpan properties with string and use [Description] attributes to convey format expectations to the model. Use JsonSchemaExporter.GetJsonSchemaAsNode in .NET 9 to validate schemas locally before deployment.

Introduction

Azure OpenAI’s structured outputs feature gives you 100% guaranteed JSON schema compliance from the model. Unlike JSON mode, which asks the model to return valid JSON but allows it to invent fields or skip required ones, structured outputs enforce your schema strictly — the model will never return a response that deviates from it.

This guide covers three approaches to structured outputs in C#:

  1. SDK directChatResponseFormat.CreateJsonSchemaFormat<T> in the OpenAI .NET SDK
  2. MEAIMicrosoft.Extensions.AI with ChatOptions.ResponseFormat
  3. Semantic KernelOpenAIPromptExecutionSettings.ResponseFormat = typeof(T)

It also covers the schema design rules that determine which C# types work, how to use [Description] attributes to guide the model, error handling for schema violations, and how to validate your schema locally before deploying.

1. Structured Outputs vs JSON Mode

Understanding the difference matters because the two features behave differently under failure conditions.

JSON mode (response_format: json_object) tells the model to return syntactically valid JSON. It will return some JSON. It may include extra fields, omit required ones, use wrong value types, or invent property names. Your deserialization code must defensively handle all of these.

Structured outputs (response_format: json_schema with strict: true) send your schema to the model and enforce it at the API level. The model cannot return a response that deviates from the schema. If the model cannot produce a valid response, the API returns an error rather than a schema-violating response.

// JSON mode — what you asked for vs what you might get
// Schema: { "title": string, "score": int, "tags": string[] }
// Actual response:
{
  "title": "Example Article",
  "rating": 8,          // Wrong field name
  "keywords": ["AI"]    // Wrong field name
}

// Structured outputs — schema is enforced
// You always get:
{
  "title": "Example Article",
  "score": 8,
  "tags": ["AI"]
}

Structured outputs require:

  • GPT-4o (model version 2024-08-06 or later) or GPT-4o-mini
  • API version 2024-08-01-preview or later

For the v2 SDK types used throughout this guide, see the Azure OpenAI SDK v2 Migration Guide for C# if you are upgrading from an older SDK version.

2. Approach 1: SDK Direct with ChatResponseFormat

The most explicit approach uses ChatResponseFormat.CreateJsonSchemaFormat<T> directly in ChatCompletionOptions. This gives you the most control and works with any version of the SDK without additional frameworks.

Setting Up the Client

using Azure;
using Azure.AI.OpenAI;
using OpenAI.Chat;

// Structured outputs require API version 2024-08-01-preview or later
var clientOptions = new AzureOpenAIClientOptions(
    AzureOpenAIClientOptions.ServiceVersion.V2024_08_01_Preview);

var azureClient = new AzureOpenAIClient(
    new Uri("https://<your-resource>.openai.azure.com/"),
    new AzureKeyCredential(Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!),
    clientOptions);

ChatClient chatClient = azureClient.GetChatClient("gpt-4o-deployment");

Defining the Schema Type

Define a C# record or class using only JSON Schema-compatible types (covered in section 5):

using System.ComponentModel;

public class ArticleAnalysis
{
    public string Title { get; set; } = string.Empty;
    public string Summary { get; set; } = string.Empty;
    public string Sentiment { get; set; } = string.Empty; // "positive", "neutral", "negative"
    public string[] KeyTopics { get; set; } = [];
    public int WordCount { get; set; }

    [Description("ISO 8601 date string, e.g. 2026-03-21")]
    public string PublishedDate { get; set; } = string.Empty;
}

Making the Request

using System.Text.Json;
using OpenAI.Chat;

var options = new ChatCompletionOptions
{
    ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat<ArticleAnalysis>(
        "article_analysis",   // Schema name — snake_case, no spaces
        strict: true)         // Enables guaranteed schema compliance
};

List<ChatMessage> messages =
[
    new SystemChatMessage("You are a precise content analysis assistant. Analyze the provided article and return a structured analysis."),
    new UserChatMessage($"Analyze this article: {articleText}")
];

ClientResult<ChatCompletion> result = await chatClient.CompleteChatAsync(messages, options);

// Access the JSON from the response
string json = result.Value.Content[0].Text;

// Deserialize — guaranteed to succeed if the API call succeeded
ArticleAnalysis analysis = JsonSerializer.Deserialize<ArticleAnalysis>(json)!;

Console.WriteLine($"Title: {analysis.Title}");
Console.WriteLine($"Sentiment: {analysis.Sentiment}");
Console.WriteLine($"Key topics: {string.Join(", ", analysis.KeyTopics)}");

The key points:

  • strict: true is required for schema compliance guarantees; without it, behaviour falls back to best-effort JSON
  • Access the response text via result.Value.Content[0].Text — note .Value to unwrap ClientResult<ChatCompletion>
  • JsonSerializer.Deserialize<T> will always succeed if the API call completed without error, because the schema is enforced server-side

Schema Name Convention

The first argument to CreateJsonSchemaFormat is the schema name. Use snake_case, no spaces, no special characters:

// ✅ Valid schema names
ChatResponseFormat.CreateJsonSchemaFormat<ArticleAnalysis>("article_analysis", strict: true)
ChatResponseFormat.CreateJsonSchemaFormat<ProductRecord>("product_record", strict: true)
ChatResponseFormat.CreateJsonSchemaFormat<OrderSummary>("order_summary", strict: true)

// ❌ Invalid — will cause a 400 error
ChatResponseFormat.CreateJsonSchemaFormat<ArticleAnalysis>("Article Analysis", strict: true)  // spaces
ChatResponseFormat.CreateJsonSchemaFormat<ArticleAnalysis>("article-analysis", strict: true)  // hyphens

3. Approach 2: Microsoft.Extensions.AI (MEAI)

Microsoft.Extensions.AI provides a unified IChatClient abstraction over multiple providers. It supports requesting JSON output via ChatOptions.ResponseFormat, but does not expose a typed structured output API in version 10.3.0. You use it with JSON mode and manual deserialization.

using Microsoft.Extensions.AI;

// Register IChatClient via DI (typically in Program.cs)
// builder.Services.AddAzureOpenAIChatClient(...);

public class ArticleService(IChatClient chatClient)
{
    public async Task<ArticleAnalysis> AnalyzeAsync(string articleText)
    {
        var options = new ChatOptions
        {
            ResponseFormat = ChatResponseFormat.Json  // JSON mode, not strict schema
        };

        // Include the schema description in the system prompt for guidance
        var messages = new List<ChatMessage>
        {
            new(ChatRole.System,
                """
                You are a content analysis assistant. Return a JSON object with these fields:
                - title (string): article title
                - summary (string): 2-3 sentence summary
                - sentiment (string): "positive", "neutral", or "negative"
                - keyTopics (string[]): main topics covered
                - wordCount (int): estimated word count
                - publishedDate (string): ISO 8601 date if found, otherwise empty string
                """),
            new(ChatRole.User, $"Analyze this article: {articleText}")
        };

        ChatResponse response = await chatClient.GetResponseAsync(messages, options);

        // MEAI does not guarantee schema compliance — deserialize defensively
        string json = response.Text;
        return JsonSerializer.Deserialize<ArticleAnalysis>(json)
            ?? throw new InvalidOperationException("Model returned invalid JSON");
    }
}

When to use MEAI:

  • Your application already uses IChatClient abstractions for portability across providers
  • You need to switch between Azure OpenAI and other providers without changing calling code
  • Schema compliance is important but you handle deviations in application logic

For strict schema guarantees with MEAI-registered clients, use the SDK-direct approach or Semantic Kernel instead.

4. Approach 3: Semantic Kernel

Semantic Kernel’s OpenAI connector has first-class structured output support. Setting ResponseFormat = typeof(T) in OpenAIPromptExecutionSettings automatically configures the API call with strict: true.

Setup

<PackageReference Include="Microsoft.SemanticKernel" Version="1.54.0" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureOpenAI" Version="1.54.0" />
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;

var builder = Kernel.CreateBuilder();

builder.AddAzureOpenAIChatCompletion(
    deploymentName: "gpt-4o-deployment",
    endpoint: "https://<your-resource>.openai.azure.com/",
    apiKey: Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!);

Kernel kernel = builder.Build();

Invoking with Structured Output

using System.Text.Json;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;

// Set ResponseFormat to the target type — SK handles the rest
var settings = new OpenAIPromptExecutionSettings
{
    ResponseFormat = typeof(ArticleAnalysis)
};

var kernelArgs = new KernelArguments(settings);

FunctionResult result = await kernel.InvokePromptAsync(
    $"Analyze this article and return a structured analysis: {articleText}",
    kernelArgs);

// Get the raw JSON string from the result
string json = result.GetValue<string>()!;

// Deserialize
ArticleAnalysis analysis = JsonSerializer.Deserialize<ArticleAnalysis>(json)!;

Using Semantic Kernel with Template Functions

For more complex scenarios involving prompt templates:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;

var analysisFunction = kernel.CreateFunctionFromPrompt(
    "Analyze the following article content and extract structured information:\n\n{{$input}}",
    new OpenAIPromptExecutionSettings
    {
        ResponseFormat = typeof(ArticleAnalysis),
        MaxTokens = 1000
    });

KernelArguments args = new()
{
    ["input"] = articleText
};

FunctionResult result = await kernel.InvokeAsync(analysisFunction, args);
string json = result.GetValue<string>()!;
ArticleAnalysis analysis = JsonSerializer.Deserialize<ArticleAnalysis>(json)!;

The SK approach is the most concise when you are already using Semantic Kernel in your application. The connector handles the schema generation and API configuration transparently.

5. Schema Design Rules

Azure OpenAI’s structured output mode accepts a subset of JSON Schema. Understanding the supported and unsupported types prevents 400 errors before they reach production.

Supported Types

C# TypeJSON Schema TypeNotes
stringstringFully supported
int, longintegerFully supported
boolbooleanFully supported
decimal, double, floatnumberFully supported
string[], List<string>array of stringFully supported
int[], List<int>array of integerFully supported
Nested class/recordobjectFully supported
enumstring with enum valuesSupported
Nullable (string?, int?)anyOf: [{type}, {type: null}]Supported

Unsupported Types

C# TypeProblemFix
DateTimeNot a JSON Schema primitiveUse string with [Description]
DateTimeOffsetNot a JSON Schema primitiveUse string with [Description]
UriSchema generator may emit non-primitive metadataUse string with [Description]
TimeSpanNo JSON Schema equivalentUse string with [Description]
GuidMay emit format metadataUse string
byte[]Complex schemaUse string (Base64) with [Description]

Working Example

using System.ComponentModel;

// ❌ Wrong — contains unsupported types
public class InvoiceRecord
{
    public string InvoiceNumber { get; set; } = string.Empty;
    public DateTime IssuedAt { get; set; }          // ❌ Not supported
    public Uri PaymentUrl { get; set; } = null!;    // ❌ Not supported
    public TimeSpan PaymentTerms { get; set; }       // ❌ Not supported
    public decimal TotalAmount { get; set; }
    public string[] LineItems { get; set; } = [];
}

// ✅ Correct — all JSON Schema primitives
public class InvoiceRecord
{
    public string InvoiceNumber { get; set; } = string.Empty;

    [Description("ISO 8601 date-time string, e.g. 2026-03-21T09:00:00Z")]
    public string IssuedAt { get; set; } = string.Empty;

    [Description("Full URL for the payment portal")]
    public string PaymentUrl { get; set; } = string.Empty;

    [Description("Payment terms as a human-readable string, e.g. 'Net 30 days'")]
    public string PaymentTerms { get; set; } = string.Empty;

    public decimal TotalAmount { get; set; }
    public string[] LineItems { get; set; } = [];
}

Nested Objects and Arrays

Nested objects and arrays of objects are fully supported:

public class OrderAnalysis
{
    public string OrderId { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;       // "pending", "shipped", "delivered"
    public LineItem[] Items { get; set; } = [];
    public ShippingInfo Shipping { get; set; } = new();
    public decimal TotalAmount { get; set; }
}

public class LineItem
{
    public string ProductName { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

public class ShippingInfo
{
    public string Carrier { get; set; } = string.Empty;
    public string TrackingNumber { get; set; } = string.Empty;

    [Description("Estimated delivery date in ISO 8601 format, e.g. 2026-03-25")]
    public string EstimatedDelivery { get; set; } = string.Empty;
}

6. Using the [Description] Attribute

The [Description] attribute from System.ComponentModel is included in the generated JSON schema as a description field. The model reads this at inference time and uses it to format output correctly.

This is particularly important for string properties that represent typed data (dates, URLs, enums, formatted values):

using System.ComponentModel;

public class ContentMetadata
{
    public string Title { get; set; } = string.Empty;

    [Description("2-3 sentence summary, written in third person")]
    public string Summary { get; set; } = string.Empty;

    [Description("One of: 'beginner', 'intermediate', 'advanced'")]
    public string Level { get; set; } = string.Empty;

    [Description("ISO 8601 date string only (no time), e.g. 2026-03-21")]
    public string PublishedDate { get; set; } = string.Empty;

    [Description("Estimated reading time as a string, e.g. '8 min read'")]
    public string ReadingTime { get; set; } = string.Empty;

    [Description("Comma-separated list of .NET SDK names and versions referenced, e.g. 'Azure.AI.OpenAI 2.1.0, OpenAI 2.1.0'")]
    public string SdkVersions { get; set; } = string.Empty;

    [Description("URL slug derived from the title, kebab-case, no special characters")]
    public string Slug { get; set; } = string.Empty;
}

After deserialization, parse the string values back to their proper .NET types in a mapping layer:

// Deserialize the model response
ContentMetadata meta = JsonSerializer.Deserialize<ContentMetadata>(json)!;

// Map to your domain model — parse strings to proper types here
var article = new Article
{
    Title = meta.Title,
    Summary = meta.Summary,
    PublishedDate = DateOnly.Parse(meta.PublishedDate),   // Parse string → DateOnly
    ReadingTime = meta.ReadingTime,
    Slug = meta.Slug
};

7. Error Handling

Schema-related errors arrive as 400 invalid_request_error responses. There are three distinct root causes, each with a specific fix.

For a complete diagnostic guide with exact error messages and fixes, see Fix Azure OpenAI Structured Output JSON Schema Errors in C#.

The pattern for handling schema errors with a fallback to JSON mode:

using Azure;
using OpenAI.Chat;
using System.Text.Json;

public async Task<ArticleAnalysis?> AnalyzeWithFallbackAsync(
    ChatClient chatClient,
    string articleText)
{
    // Try structured outputs first
    try
    {
        var options = new ChatCompletionOptions
        {
            ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat<ArticleAnalysis>(
                "article_analysis",
                strict: true)
        };

        ClientResult<ChatCompletion> result = await chatClient.CompleteChatAsync(
            [new UserChatMessage($"Analyze this article: {articleText}")],
            options);

        return JsonSerializer.Deserialize<ArticleAnalysis>(result.Value.Content[0].Text);
    }
    catch (RequestFailedException ex) when (ex.Status == 400)
    {
        // Log the schema error for investigation
        Console.Error.WriteLine($"Structured output schema error: {ex.Message}");

        // Fall back to JSON mode — no schema guarantee but more permissive
        try
        {
            var fallbackOptions = new ChatCompletionOptions
            {
                ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat()
            };

            ClientResult<ChatCompletion> fallbackResult = await chatClient.CompleteChatAsync(
                [
                    new SystemChatMessage(
                        "Return a JSON object with: title (string), summary (string), " +
                        "sentiment (string), keyTopics (string[]), wordCount (int), publishedDate (string)"),
                    new UserChatMessage($"Analyze this article: {articleText}")
                ],
                fallbackOptions);

            string json = fallbackResult.Value.Content[0].Text;
            return JsonSerializer.Deserialize<ArticleAnalysis>(json);
        }
        catch (JsonException jsonEx)
        {
            Console.Error.WriteLine($"JSON fallback deserialization failed: {jsonEx.Message}");
            return null;
        }
    }
}

Key error scenarios:

ErrorRoot CauseFix
response_format.json_schema is not supported for this API versionWrong API versionSet ServiceVersion.V2024_08_01_Preview or later
Invalid schema for response_format 'json_schema': unsupported typeDateTime, Uri, or TimeSpan in schemaReplace with string + [Description]
Structured outputs are not supported with the current modelGPT-4 Turbo or non-OpenAI modelSwitch to GPT-4o (2024-08-06+) or use JSON mode

8. Testing and Validating Your Schema

Before deploying a new structured output type to production, validate the schema it generates using JsonSchemaExporter from System.Text.Json.Schema (.NET 9+):

using System.Text.Json;
using System.Text.Json.Schema;

// Generate the schema your type will produce
JsonNode schema = JsonSchemaExporter.GetJsonSchemaAsNode(
    new JsonSerializerOptions(),
    typeof(ArticleAnalysis));

// Pretty-print for inspection
string schemaJson = schema.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(schemaJson);

Example output for ArticleAnalysis:

{
  "type": "object",
  "properties": {
    "title": { "type": "string" },
    "summary": { "type": "string" },
    "sentiment": { "type": "string" },
    "keyTopics": {
      "type": "array",
      "items": { "type": "string" }
    },
    "wordCount": { "type": "integer" },
    "publishedDate": {
      "type": "string",
      "description": "ISO 8601 date string, e.g. 2026-03-21"
    }
  }
}

What to look for in the output:

  • Any $ref entries — these indicate complex types that may not be supported
  • Any format annotations — these are often produced by DateTime and can cause 400 errors
  • Any property with a non-primitive typeobject within anyOf or oneOf may require additional testing

Add a unit test or integration test that generates and inspects the schema for every type used with structured outputs:

[Fact]
public void ArticleAnalysis_SchemaContainsOnlySupportedTypes()
{
    var options = new JsonSerializerOptions();
    JsonNode schema = JsonSchemaExporter.GetJsonSchemaAsNode(options, typeof(ArticleAnalysis));
    string schemaJson = schema.ToJsonString();

    // Verify no unsupported types leaked into the schema
    Assert.DoesNotContain("\"format\"", schemaJson);   // DateTime format annotations
    Assert.DoesNotContain("\"$ref\"", schemaJson);     // Complex type references
    Assert.DoesNotContain("\"anyUri\"", schemaJson);   // URI type hints
}

This test will catch type issues at build time rather than at runtime when the API rejects the schema.

Summary

Structured outputs in Azure OpenAI give you the reliability of a strongly-typed API response without building fragile parsing logic. The right approach depends on your stack:

  • SDK direct (ChatResponseFormat.CreateJsonSchemaFormat<T>) — explicit, portable, works anywhere you have an AzureOpenAIClient
  • MEAI — use for provider portability; note it uses JSON mode, not strict schemas, in version 10.3.0
  • Semantic Kernel (ResponseFormat = typeof(T)) — most concise when SK is already in your stack

Schema design is where most failures occur. Keep types simple: only JSON Schema primitives, replace DateTime/Uri/TimeSpan with string, and use [Description] attributes to communicate format expectations. Validate with JsonSchemaExporter before every deployment.

⚠ Production Considerations

  • Nullable reference types in your schema record (string? field) generate anyOf: [{type: string}, {type: null}] in the schema — this is supported. But required non-nullable reference type properties that you initialize with defaults (= string.Empty) are still marked required in the schema. Test every new type with JsonSchemaExporter.GetJsonSchemaAsNode before deploying to avoid runtime 400 errors.
  • Structured output adds a schema validation step to each API response. For deeply nested schemas (5+ levels, 50+ properties), this adds 1–3% latency per call. Consider flattening your response schema when latency is critical. Also note that the schema is sent with every request — large schemas increase prompt token count and cost.
  • The strict schema generated from your C# type is cached by the model endpoint during a session. If you change the type between requests in a long-lived client, you may receive schema mismatch errors. Create a new ChatClient or restart the session after changing a structured output type.

Enjoying this article?

Get weekly .NET + AI insights delivered to your inbox. No spam.

Subscribe Free →

🧠 Architect’s Note

Prefer structured outputs over JSON mode for all production workloads — the reliability guarantee eliminates an entire category of deserialization failures and null-safety bugs. Design your schema types as thin DTOs separate from your domain model: they exist solely to communicate with the API. Parse string properties (dates, URLs, durations) back to their proper .NET types in a mapping layer after deserialization. This keeps the schema portable and the domain model clean.

AI-Friendly Summary

Summary

Azure OpenAI structured outputs guarantee 100% JSON schema compliance from GPT-4o and GPT-4o-mini when using API version 2024-08-01-preview or later. In the OpenAI .NET SDK, use ChatResponseFormat.CreateJsonSchemaFormat<T>(name, strict: true) in ChatCompletionOptions. Semantic Kernel supports structured outputs via ResponseFormat = typeof(T) in OpenAIPromptExecutionSettings. Schema types must use only JSON Schema primitives — string, int, bool, decimal, enums, nested objects, and arrays. Replace DateTime, Uri, and TimeSpan properties with string and use [Description] attributes to convey format expectations to the model. Use JsonSchemaExporter.GetJsonSchemaAsNode in .NET 9 to validate schemas locally before deployment.

Key Takeaways

  • Structured outputs guarantee 100% JSON schema compliance; JSON mode only requests valid JSON without schema adherence
  • Use ChatResponseFormat.CreateJsonSchemaFormat<T>(name, strict: true) in ChatCompletionOptions for SDK-direct calls
  • Semantic Kernel enables structured outputs via ResponseFormat = typeof(T) in OpenAIPromptExecutionSettings
  • Schema types must use JSON Schema primitives only — replace DateTime/Uri/TimeSpan with string
  • Add [Description] attributes from System.ComponentModel to string properties to guide model formatting
  • Validate schemas locally with JsonSchemaExporter.GetJsonSchemaAsNode before deploying

Implementation Checklist

  • Confirm deployment uses GPT-4o (2024-08-06+) or GPT-4o-mini
  • Set AzureOpenAIClientOptions.ServiceVersion to V2024_08_01_Preview or later
  • Design your response type using only JSON Schema primitives (string, int, bool, decimal, enums, arrays, nested objects)
  • Replace DateTime, Uri, and TimeSpan properties with string and add [Description] attributes
  • Call ChatResponseFormat.CreateJsonSchemaFormat<T>(name, strict: true) in ChatCompletionOptions
  • Deserialize the response with JsonSerializer.Deserialize<T>(completion.Value.Content[0].Text)
  • Validate schema locally with JsonSchemaExporter.GetJsonSchemaAsNode before production deployment
  • Add error handling for 400 invalid_request_error with fallback to JSON mode if needed

Frequently Asked Questions

What is the difference between JSON mode and structured outputs in Azure OpenAI?

JSON mode (response_format: json_object) asks the model to return valid JSON but does not guarantee schema compliance — fields can be missing, have wrong types, or include extra properties. Structured outputs (response_format: json_schema with strict: true) enforce 100% schema compliance. The model will never return a response that deviates from the provided schema. Prefer structured outputs for all production code where downstream logic depends on specific field names and types.

Which Azure OpenAI models support structured outputs?

Structured outputs require GPT-4o (model version 2024-08-06 or later) or GPT-4o-mini. GPT-4 Turbo, DeepSeek-R1, Llama 3.x, and other non-OpenAI models available through Azure AI Foundry support JSON mode but not strict schema compliance. Check your deployment model version in Azure AI Foundry before enabling structured outputs.

What API version does Azure OpenAI require for structured outputs?

Structured outputs require API version 2024-08-01-preview or later. Set the version via AzureOpenAIClientOptions.ServiceVersion when constructing AzureOpenAIClient. The minimum is V2024_08_01_Preview; prefer V2024_10_01_Preview or later for new projects.

Why can't I use DateTime in a structured output schema?

Azure OpenAI's strict JSON Schema mode only accepts primitive JSON Schema types: string, number, boolean, integer, array, object, and null. DateTime, DateTimeOffset, Uri, and TimeSpan do not map to JSON Schema primitives and will cause a 400 invalid_request_error. Replace them with string properties and use the [Description] attribute to tell the model the expected format, for example 'ISO 8601 date string, e.g. 2026-03-21'.

How do I use structured outputs with Semantic Kernel in C#?

Set ResponseFormat = typeof(YourRecord) in OpenAIPromptExecutionSettings. The Semantic Kernel OpenAI connector automatically translates this to a structured output API call with strict: true. Retrieve the result with result.GetValue<string>() and then deserialize with JsonSerializer.Deserialize<YourRecord>(). The same type restrictions apply — no DateTime, Uri, or TimeSpan properties.

How do I validate my structured output schema before sending it to Azure OpenAI?

Use JsonSchemaExporter.GetJsonSchemaAsNode(new JsonSerializerOptions(), typeof(YourRecord)) from System.Text.Json.Schema in .NET 9. This generates the exact JSON schema your type will produce. Inspect it for unsupported properties such as those producing $ref, format annotations, or non-primitive types before sending the request.

Does Microsoft.Extensions.AI (MEAI) support typed structured outputs?

MEAI 10.3.0 supports ChatResponseFormat.Json mode by setting it in ChatOptions, which asks the model for valid JSON. However, MEAI does not expose a generic typed structured output API — you must still call JsonSerializer.Deserialize<T>() manually after receiving the response. For strict schema guarantees, use the OpenAI SDK directly or Semantic Kernel.

Track your progress through this learning path.

You Might Also Enjoy

Was this article useful?

Feedback is anonymous and helps us improve content quality.

Discussion

Engineering discussion powered by GitHub Discussions.

#Structured Output #JSON Schema #Azure OpenAI #Semantic Kernel #Type Safety #.NET AI