The fields are not known until the application runs.
When each customer defines the fields it wants extracted, a compiled class per customer means a release for every new field. With IntentSharp the fields are a JSON schema read at run time: one call asks the model for JSON in that shape, and your validators check each answer.
What it costs today
A typed C# class is the right tool when the shape is known at compile time. When customers configure their own fields, the list of classes grows with the list of customers, a switch picks the class for each one, and a new field for one customer is a code change, a build and a deployment.
The usual way out is a dictionary of strings with a hand-written check for each field, which loses the types and still needs the parsing, the checks and the retries around every call.
How IntentSharp handles it
Two customers keep their fields as JSON schemas in settings, each with fields of its own. One TransformAsync(input, intent, schema, options) call per customer asks for JSON in that customer's shape. Your validator, SchemaRules, checks each schema's required fields and enum lists: the first customer's first answer writes the currency as a symbol, and the retry with the finding fixes it.
// Each customer's fields, as a JSON schema kept in your settings
// or database. A new field is an edit, not a build.
var customerFields = new Dictionary<string, string>
{
["acme"] = """
{"type": "object",
"properties": {
"vendor": {"type": "string"},
"total": {"type": "number"},
"currency": {"type": "string", "enum": ["EUR", "GBP"]}},
"required": ["vendor", "total", "currency"]}
""",
["globex"] = """
{"type": "object",
"properties": {
"supplier": {"type": "string"},
"invoiceNumber": {"type": "string"},
"dueDate": {"type": "string"}},
"required": ["supplier", "invoiceNumber", "dueDate"]}
""",
};var email = "Invoice INV-3307 from Fabrikam Tools: 12 torque wrenches, total 1,860.00 EUR, due by 2026-12-01."; // The core checks that each reply is JSON and not null. The// schema's own rules are for your validator: this one checks its// required fields and its enum lists.static IEnumerable<string> SchemaRules(JsonNode schema, JsonNode value){ foreach (var field in schema["required"]!.AsArray()) if (value[(string)field!] is null) yield return $"{field} is required."; foreach (var (name, rule) in schema["properties"]!.AsObject()) if (rule?["enum"] is JsonArray list && value[name] is { } given && !list.Any(a => JsonNode.DeepEquals(a, given))) yield return $"{name} is {given}; allowed: " + $"{string.Join(", ", list)}.";} var intent = "Extract these fields from the invoice email.";foreach (var (customer, fieldsJson) in customerFields){ var schema = JsonNode.Parse(fieldsJson)!; var options = new AIRequestOptions() .Validate<JsonNode>(value => SchemaRules(schema, value)); results[customer] = await client.TransformAsync( email, intent, schema, options);}acme, fields: vendor, total, currency [Validator] attempt 1: currency is €; allowed: EUR, GBP. Success: true, after 2 attempts Value: {"vendor":"Fabrikam Tools","total":1860.00,"currency":"EUR"}globex, fields: supplier, invoiceNumber, dueDate Success: true, after 1 attempt Value: {"supplier":"Fabrikam Tools","invoiceNumber":"INV-3307","dueDate":"2026-12-01"}Compiled and run with every build of this site; the model's answers are scripted, and a pass means the answer met the checks, not that it is right.
What stays in your control
The schemas are data you own, and so are the checks that hold an answer to them.
- What the core checks. For a schema chosen at run time, IntentSharp checks only that the reply is JSON and not null. Constraint keywords such as
required,enum,minimumorpatternare checked by your validators, asSchemaRulesdoes here, or, for an intent with a schema shape, by the catalog (next point). - When the catalog checks the whole schema. An intent with a
schemashape, in an intent file or defined in code withInMemoryIntentCatalog, is checked against its whole schema on every reply, limits such asminimumandpatternincluded. - Your validator's findings go back. A failed check is retried with the previous reply and the findings, within the attempts you allow, so keep secrets and personal data out of a validator's messages.
- Fields the input may not give. The model must fill every schema property, so give one the input may leave out a
typethat listsnull, or the model invents it. - What you get back. An
AIResult<JsonNode>:Success, the JSON value, every finding inIssuesand every attempt inAttempts. - A pass is not a proof. A pass means the reply is JSON, is not null and met your validators, not that the values in it are right.
Read next
How it works
What is checked for a type, for your rules and for a schema chosen at run time.
Intent file reference
The schema shape and the other shapes an intent can return.
Running intents from files
Intents defined in code, and the checks and types a file may name.
More problems and their answers: Solutions.
Let one customer define its own fields.
dotnet add package IntentSharp --prerelease