s into JSON Schema, injecting that schema into the system prompt, and deserializing the response into the requested type.
Step 1: Define the Output Contract
Start by declaring the expected shape as a C# record or class. Use nullable reference types, explicit enums, and collection types that match the domain. Avoid deep nesting; flat structures deserialize more reliably and consume fewer tokens.
public enum SupportCategory
{
Billing,
Technical,
AccountAccess,
FeatureRequest,
Unclassified
}
public sealed class TicketClassification
{
public SupportCategory Category { get; init; }
public double ConfidenceScore { get; init; }
public string PrimaryIssue { get; init; } = string.Empty;
public List<string> RecommendedActions { get; init; } = [];
}
Modern .NET AI libraries expose generic invocation methods that accept the target type. The framework extracts type metadata, generates a JSON Schema, and appends it to the request payload. The model is constrained to output that matches the schema.
public interface IClassificationAgent
{
Task<AgentResponse<T>> RunAsync<T>(string prompt, CancellationToken ct = default);
}
// Usage
var prompt = """
Analyze the following support message and classify it.
Return only the structured classification. Do not include conversational text.
Message: {userInput}
""";
AgentResponse<TicketClassification> response =
await classifierAgent.RunAsync<TicketClassification>(prompt, ct);
TicketClassification classification = response.Result;
Step 3: Handle Deserialization and Validation
The framework returns AgentResponse<T>, which contains the deserialized object and metadata. Deserialization failures are caught at the framework level, but semantic validation remains the application's responsibility. Implement a validation layer that checks ranges, enum constraints, and business rules before routing.
public static class ClassificationValidator
{
public static void EnforceRules(TicketClassification classification)
{
if (classification.ConfidenceScore is < 0.0 or > 1.0)
throw new ArgumentOutOfRangeException(nameof(classification.ConfidenceScore),
"Confidence must be between 0 and 1.");
if (string.IsNullOrWhiteSpace(classification.PrimaryIssue))
throw new InvalidOperationException("Primary issue description is required.");
if (classification.RecommendedActions.Count is 0 or > 5)
throw new InvalidOperationException("Recommendations must contain 1 to 5 items.");
}
}
Architecture Rationale
- Generic Invocation (
RunAsync<T>): Decouples prompt engineering from type handling. The framework manages schema generation, reducing boilerplate and ensuring consistency across agents.
- Flat POCOs: Deep object graphs increase token consumption and deserialization failure rates. Flat structures with explicit enums and bounded collections improve reliability.
- Separation of Concerns: Deserialization handles shape; validation handles truth. This boundary allows independent testing, mocking, and metric collection.
- Framework Schema Injection: Modern .NET AI providers automatically convert C# type metadata into JSON Schema. This eliminates manual prompt formatting and ensures the model receives explicit constraints.
Pitfall Guide
1. Assuming Structural Validity Equals Semantic Correctness
Explanation: A successfully deserialized object satisfies the schema but may contain nonsensical values. The model can return a valid TicketClassification with a 0.95 confidence score and a billing category for a password reset request.
Fix: Treat deserialization as the first gate, not the final authority. Implement domain validation that checks logical consistency, cross-field constraints, and business rules before accepting the result.
2. Over-Nesting Complex Types Without Schema Limits
Explanation: Deeply nested objects or unbounded collections increase token usage and deserialization failure rates. The model may truncate output or misalign fields when the schema exceeds context window limits.
Fix: Flatten structures. Use bounded collections (List<T> with explicit max counts), prefer enums over free-text strings, and split complex extractions into sequential agent calls if necessary.
3. Skipping Fallback Mechanisms for Deserialization Failures
Explanation: Network timeouts, model version changes, or prompt drift can cause schema mismatches. Without fallbacks, the application throws unhandled exceptions or enters dead states.
Fix: Wrap generic invocations in retry policies with exponential backoff. Implement a fallback path that returns a default classification or escalates to a human review queue when deserialization fails repeatedly.
4. Ignoring Token Budget for Schema Injection
Explanation: The framework injects the JSON Schema into the prompt. Large schemas consume context tokens, leaving less room for user input and reducing model accuracy.
Fix: Audit schema size. Remove unused properties, use concise property names, and leverage schema references or $defs if the provider supports them. Monitor token usage in production and adjust schema complexity accordingly.
5. Treating Enum Constraints as Hard Guarantees
Explanation: Models may return enum values not defined in the schema, especially when prompted loosely. The framework may fail deserialization or map to an unknown state.
Fix: Define a fallback enum value (e.g., Unclassified or Unknown). Configure the framework to map unrecognized strings to the fallback. Validate enum values explicitly in the application layer.
6. Bypassing Domain Validation Post-Deserialization
Explanation: Developers often assume that because the object deserializes, it's ready for production use. This skips critical checks like confidence thresholds, rate limits, or compliance rules.
Fix: Implement a validation pipeline that runs after deserialization. Use FluentValidation or DataAnnotations for declarative rules. Log validation failures separately from deserialization errors to isolate model behavior from business logic.
Explanation: Developers often repeat schema details in the prompt text ("Return a JSON object with fields X, Y, Z"). This duplicates effort, increases token usage, and creates maintenance drift when types change.
Fix: Rely on framework schema injection. Keep prompts focused on intent and constraints. Let the type definition drive the structure. Update the C# type, and the prompt adapts automatically.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple field extraction (name, date, ID) | Typed schema contract | Compile-time safety, zero parsing overhead | Low (minimal token increase) |
| Complex routing with multiple intents | Enum-based classification + confidence threshold | Deterministic switching, fallback to human review | Medium (requires validation layer) |
| High-stakes compliance or financial data | Structured output + strict domain validation + audit logging | Ensures business rules are enforced, not just shape | High (validation + monitoring infrastructure) |
| Chat-only UI with no downstream logic | Raw string or markdown formatting | No integration boundary, user-facing prose is acceptable | Low (no schema overhead) |
Configuration Template
// Program.cs / Dependency Injection setup
builder.Services.AddAiAgent<IClassificationAgent, ClassificationAgent>(options =>
{
options.Model = "gpt-4o-mini";
options.Temperature = 0.1;
options.MaxTokens = 500;
options.ResponseFormat = ResponseFormat.JsonSchema; // Enables structured output
});
builder.Services.AddScoped<TicketRouter>();
// TicketRouter.cs
public class TicketRouter(IClassificationAgent classifier, ILogger<TicketRouter> logger)
{
public async Task<RouteDecision> ClassifyAndRouteAsync(string userInput, CancellationToken ct)
{
var response = await classifier.RunAsync<TicketClassification>(
$"Classify this support message: {userInput}", ct);
var classification = response.Result;
ClassificationValidator.EnforceRules(classification);
if (classification.ConfidenceScore < 0.75)
{
logger.LogWarning("Low confidence classification: {Score}", classification.ConfidenceScore);
return RouteDecision.EscalateToHuman;
}
return classification.Category switch
{
SupportCategory.Billing => RouteDecision.RouteToBillingQueue,
SupportCategory.Technical => RouteDecision.RouteToEngineeringQueue,
SupportCategory.AccountAccess => RouteDecision.RouteToSecurityQueue,
_ => RouteDecision.RouteToGeneralQueue
};
}
}
Quick Start Guide
- Define the contract: Create a C# record or class with the exact fields your application needs. Use enums for categorical data and bounded collections for lists.
- Register the agent: Configure your .NET AI provider with
ResponseFormat.JsonSchema and inject the agent into your service layer.
- Invoke generically: Call
RunAsync<T>() with your prompt. The framework handles schema generation and deserialization automatically.
- Validate the result: Run domain-specific checks on confidence scores, field ranges, and business rules before routing or persisting.
- Test independently: Mock
AgentResponse<T> with synthetic payloads to verify routing logic, validation rules, and fallback paths without hitting the model.