Documentation
Table of Contents

Typed output

You ask for a .NET type, and the answer comes back as that type or as a failed result: AIAsync<T>, TransformAsync<T> and the intent runtime's RunAsync<T> all return an AIResult<T>. This page covers how your type reaches the model, which types the model receives as a strict schema, and the request options that shape an answer.

Your type becomes a strict schema

For most result types, the model receives a strict JSON schema built from your type, and the provider is asked to answer in that schema (structured output, in the vendors' terms). The answer comes back inside a small JSON object, {"value": ...}, so a number, a list or a record all travel the same way, and it is then read into your type and checked.

// A .NET type: the model is sent a strict schema built from PurchaseRequest, and the reply must parse into it.
// A required member missing from the reply is an issue, not a default value.
AIResult<PurchaseRequest> order = await email.AIAsync<PurchaseRequest>(client, "Extract the purchase request from this email.");

// A shape defined at run time: a JSON schema, and the value comes back as JSON.
var schema = JsonNode.Parse("""{"type": "object", "properties": {"sku": {"type": "string"}, "quantity": {"type": "integer"}}}""")!;
AIResult<JsonNode> line = await client.TransformAsync(email, "Extract the SKU and the quantity.", schema);

These types get a strict schema:

  • int, long, double, decimal, bool and the other numeric types; string, Guid and Uri;
  • DateTime, DateTimeOffset, DateOnly, TimeOnly and TimeSpan;
  • enums;
  • arrays, lists and other collections of these;
  • classes and records whose public properties are all of these, nested to any depth.

In the schema, property names are camelCase, every property is required, and no other properties are allowed.

Some types are described instead

A strict schema cannot express every type. For these, no schema is sent: the request asks for JSON and describes your type by its name and its members, and the answer is read into your type and checked in the same way when it comes back.

  • object, JsonElement, JsonNode, JsonObject and JsonDocument;
  • dictionaries;
  • a collection with no element type, such as a plain IEnumerable;
  • a type that contains itself, directly or through its members;
  • a class or record with a public property of one of these types, unless the property is marked [JsonIgnore].

Without a schema, the provider does not hold the answer to your type's shape; only the checks do. To keep a strict schema, give a dictionary's data a type of its own, such as a list of key and value records. For a result keyed by your own dictionary's keys, AIMapValuesAsync keeps the keys and checks them; see Collections.

A text result is the reply itself

For string, the model is asked for plain text, with no schema and no JSON. The reply text is the value, and your rules run on it. Use a text result for prose, and a type for anything your code reads.

Required members, nulls and enums

The answer is read strictly:

  • Required members. A member is required when it is non-nullable and settable, or set by a constructor parameter with no default value. A reference type counts as non-nullable only in code with nullable annotations on. A required member missing from the answer is an issue, not a default value.
  • Nulls. A null answer, the whole value, passes only for a nullable value type, such as int?. For any other type, a null answer is an issue. A successful result never holds a null Value unless T allows one. A null member passes only when the member is nullable, such as int? or string?; in code without nullable annotations, a member of a reference type counts as nullable.
  • Enums. An enum value must be one of your enum's names. A number is refused, and so is a combination of names that is not a member, unless the enum is marked [Flags].
  • Dates. Dates and times travel as ISO 8601 strings, such as 2026-11-15 for a DateOnly.

A member that may honestly have no value, such as a delivery date an email does not give, should be nullable, such as DateOnly?. A strict schema has no optional members, so a non-nullable member forces the model to invent a value.

Ranges, lengths and patterns are rules

The strict schema carries the types and the property names, not the constraints: minimums and maximums, lengths, patterns and item counts are left out, because the providers' strict modes refuse them. Check them in your rules instead, with Require<T> or Validate<T>, or with minimum, maxLength, pattern and the other rules of an intent file. A failed rule goes back to the model on the next attempt. See Results and verification.

A shape chosen at run time

When the shape is not known until the program runs, such as a set of fields per customer kept in a database, pass a JSON schema instead of a type, to the TransformAsync overload that takes a JsonNode schema. It returns an AIResult<JsonNode>. The schema is sent strict, as above, so every property must be filled in. Mark a property that may have no value as nullable in the schema, with a type that lists null beside the property's own type.

For a shape chosen at run time, the checks confirm only that the reply is JSON and not null. Your validators check the rest, or an intent file whose shape is a schema, which checks the answer against the full schema. The Use cases page shows one schema per customer.

The options that shape an answer

These request options change what the model is asked for. All but Temperature are also fields of an intent file.

Option What it does
ExpectedOutputDescription Says in prose what a good answer looks like, where the type alone is not enough ("ISO 4217 currency codes", "T-SQL dialect").
SystemInstructions Adds your own instructions to the system message: the role, the audience, the house style.
Examples Worked examples: an input and the exact answer you expect for it. They steer the format and the judgement.
MaxOutputTokens The longest reply, in tokens, for each model call. A reply cut off at this limit fails its attempt.
Temperature The sampling temperature, from 0 to 2, for providers that accept one. Unset, none is sent. The built-in Anthropic provider sends it only when its SendTemperature is on.
Model, Provider The model to ask, and the provider by name. Naming a provider turns fallback off for the request. A model named here is sent to the fallback too, so name one both providers accept.

Write an example's output exactly as the model should answer: plain text for a text result, and the answer inside {"value": ...} for any other type. Examples steer; they never replace the checks, so an answer that copies an example and breaks a rule still fails. Here every option but Model and Provider is set on one request:

var options = new AIRequestOptions
{
    ExpectedOutputDescription = "Contact is the sender's first name, then the company in brackets.",
    SystemInstructions = "You read purchase emails for the Northwind account team.",
    MaxOutputTokens = 512,
    Temperature = 0,
};
// An example's output is written as the answer itself: {"value": ...} for any result other than text.
options.Examples.Add(new IntentExample(
    "Hello, Sam at Contoso here. Please send 12 units of B-220 by 2026-12-01.",
    """{"value": {"contact": "Sam (Contoso)", "sku": "B-220", "quantity": 12, "deliverBy": "2026-12-01"}}"""));

var order = await email.AIAsync<PurchaseRequest>(client, "Extract the purchase request from this email.", options);
Scripted model answer: {"value": {"contact": "Dana (Northwind)", "sku": "A-1192", "quantity": 40, "deliverBy": "2026-11-15"}}
Success: true
Value: PurchaseRequest { Contact = Dana (Northwind), Sku = A-1192, Quantity = 40, DeliverBy = 2026-11-15 }
Issues: none
The request carried your output description, your system instructions, the example's input, the example's output, a strict schema built from PurchaseRequest, MaxOutputTokens 512 and Temperature 0.

The answer in this output comes from a scripted model, not a real one. What is sent lists where each option appears in the request.

Next