Structured output gives application code a predictable shape. It does not prove that the values inside that shape are valid.
A model can return JSON that matches the schema and still produce an unsupported category, an impossible range, or a high-confidence decision that should not be executed automatically. Deserialization is only the first boundary.
Validate the typed result with normal C# before using it:
public sealed record IntentResult(
string? Intent,
double Confidence);
public enum SupportedIntent
{
Support,
Billing,
Other
}
static (SupportedIntent Intent, double Confidence) Validate(
IntentResult result)
{
if (!double.IsFinite(result.Confidence) ||
result.Confidence is < 0 or > 1)
{
throw new InvalidOperationException(
"Confidence must be between 0 and 1.");
}
return result.Intent?.Trim().ToLowerInvariant() switch
{
"support" => (SupportedIntent.Support, result.Confidence),
"billing" => (SupportedIntent.Billing, result.Confidence),
"other" => (SupportedIntent.Other, result.Confidence),
_ => (SupportedIntent.Other, 0)
};
}
Keep Intent as a raw string at the deserialization boundary instead of binding it directly to SupportedIntent. The validator can then apply an explicit allow-list and choose a controlled fallback instead of turning an unknown model value into a generic deserialization failure.
In this example, an unknown intent falls back to Other, but an invalid confidence rejects the result because replacing it would hide a broken numeric contract.
The validation should reflect the decision the application is about to make. Check allow-listed values, ranges, required relationships, tenant ownership, and any domain invariant that matters downstream. A valid route name does not authorize the user to perform that route’s action, and a valid tool name does not make the tool call safe.
Existing .NET validation mechanisms can still help, but they are not automatic just because the value came from a model. For example, annotate DTO properties with DataAnnotations such as [Range(0.0, 1.0)] and explicitly run validation after deserialization. Keep domain fallbacks, cross-field rules, and authorization checks in explicit application validation.
Schema-constrained output still helps. It reduces malformed responses and makes parsing failures explicit. Keep a controlled policy for those failures, such as returning a safe fallback, asking the model for one bounded repair attempt, or routing the request for review.
Do not silently clamp every invalid value. Clamping can be reasonable for presentation data, but it can also hide a broken contract. Reject or fall back when changing the value would alter the meaning of the model’s decision.
Treat structured model output like any other untrusted input. Validate it before routing, writing state, calling tools, sending messages, or making irreversible decisions.