StructuredChat 0.0.5
See the version list below for details.
dotnet add package StructuredChat --version 0.0.5
NuGet\Install-Package StructuredChat -Version 0.0.5
<PackageReference Include="StructuredChat" Version="0.0.5" />
<PackageVersion Include="StructuredChat" Version="0.0.5" />
<PackageReference Include="StructuredChat" />
paket add StructuredChat --version 0.0.5
#r "nuget: StructuredChat, 0.0.5"
#:package StructuredChat@0.0.5
#addin nuget:?package=StructuredChat&version=0.0.5
#tool nuget:?package=StructuredChat&version=0.0.5
StructuredChat - Complete User Guide
Overview
StructuredChat is a .NET library that provides a fluent interface for extracting structured data from AI chat responses using schema validation. It wraps the Microsoft.Extensions.AI IChatClient interface and ensures responses conform to predefined formats through JsonSchema.Net validation.
The library leverages JsonSchema.Net and JsonSchema.Net.Generation for schema creation and validation, allowing you to use schema attributes on your classes or provide custom schemas directly.
Features
- ✅ Type-safe extraction - Extract structured data with compile-time type safety
- ✅ Schema validation - Automatic validation using JsonSchema.Net
- ✅ Retry mechanism - Built-in retry logic with exponential backoff
- ✅ Fluent API - Easy-to-use configuration interface
- ✅ Multiple data types - Support for enums, numbers, dates, lists, and custom objects
- ✅ Multi-framework support - Works with .NET Standard 2.0, .NET 8, 9, and 10
Why Schema Validation Matters
StructuredChat validates every AI response against a JSON schema to ensure data reliability and type safety. Large language models can occasionally produce responses that deviate from expected formats—missing required fields, using wrong data types, or providing values outside valid ranges. By validating responses before returning them to your application, StructuredChat acts as a safety barrier that prevents malformed data from propagating through your system.
The library uses two types of schemas: internal built-in schemas automatically generated for specialized question types (yes/no, enums, numbers, dates, etc.) and external schemas that you provide for custom structured data extraction. The built-in retry mechanism leverages this validation to automatically request corrections from the model, sending specific error feedback that helps the AI learn from its mistakes and produce compliant responses.
Suppose validation fails after all retry attempts are exhausted. In that case, the result is returned with IsValid = false and detailed ValidationErrors describing what went wrong—allowing you to handle the failure gracefully in your application. Alternatively, you can configure the client with WithSchemaValidationException() to throw a SchemaValidationException instead, ensuring that invalid data never silently passes through. This approach transforms unreliable text generation into predictable, structured data extraction that you can confidently integrate into production applications.
Installation
dotnet add package StructuredChat
<PackageReference Include="StructuredChat" />
Dependencies:
- Microsoft.Extensions.AI.Abstractions
- JsonSchema.Net
- JsonSchema.Net.Generation
Getting Started
First, create an instance of IChatClient (using your preferred AI provider), then wrap it with StructuredChat:
using Microsoft.Extensions.AI;
using StructuredChat;
// Create your AI client (example using any provider)
IChatClient chatClient = /* your chat client instance */;
// IChatClient client = new OpenAIClient("api-key").GetChatClient("gpt-4o").AsIChatClient();
// Wrap it with StructuredChat
var structuredClient = chatClient.AsStructuredChatClient();
Configuration Options
You can configure various parameters using a fluent API:
var client = chatClient.AsStructuredChatClient()
.WithSystemPrompt("You are a helpful assistant.")
.WithTemperature(0.7f)
.WithMaxOutputTokens(1000)
.WithTopK(40)
.WithTopP(0.9f)
.WithFrequencyPenalty(0.5f)
.WithPresencePenalty(0.5f)
.WithMaxRetries(5)
.WithRetryDelay(2000)
.WithSchemaValidationException(); // Throw exception on validation failure
Configuration Methods
| Method | Parameter Type | Description | Default |
|---|---|---|---|
WithSystemPrompt(string) |
string | Set custom system prompt | Data extraction prompt that prevents fabrication |
WithTemperature(float) |
float | Control randomness (0.0 - 2.0) | null (provider default) |
WithMaxOutputTokens(int) |
int | Limit response length | null (provider default) |
WithTopK(int) |
int | Limit vocabulary for sampling | null (provider default) |
WithTopP(float) |
float | Nucleus sampling threshold (0.0 - 1.0) | null (provider default) |
WithFrequencyPenalty(float) |
float | Reduce repetition (-2.0 to 2.0) | null (provider default) |
WithPresencePenalty(float) |
float | Encourage topic diversity (-2.0 to 2.0) | null (provider default) |
WithMaxRetries(int) |
int | Retry attempts on validation failure | 3 |
WithRetryDelay(int) |
int | Delay between retries (milliseconds) | 1000 |
WithSchemaValidationException() |
- | Throw SchemaValidationException on validation failure |
false (returns invalid result) |
Notes:
- Temperature: Lower values (0.0-0.3) for deterministic/factual responses, higher (0.7-1.0) for creative responses
- Frequency/Presence Penalty: Positive values discourage repetition, negative values encourage it
- Retry Delay: Increases exponentially with each retry attempt (delay × attempt number)
Retry Mechanism
StructuredChat includes a built-in retry mechanism that automatically retries failed schema validations:
How It Works
- Automatic Retry: When a response fails schema validation, the library automatically retries
- Exponential Backoff: Each retry waits progressively longer (delay × attempt number)
- Error Feedback: Validation errors are sent back to the AI model for correction
- Configurable: Both retry count and delay are configurable
Example
var client = chatClient.AsStructuredChatClient()
.WithMaxRetries(5) // Try up to 5 times
.WithRetryDelay(1000); // Start with 1 second, then 2s, 3s, etc.
// If validation fails, it will automatically retry with error feedback
var result = await client.AskNumberAsync("What's the price?", minimum: 0, maximum: 1000);
Error Handling Modes
Default Mode (Returns Invalid Result)
var client = chatClient.AsStructuredChatClient()
.WithMaxRetries(3);
var result = await client.AskStructuredAsync<Product>("Extract product info");
if (!result.IsValid)
{
foreach (var error in result.ValidationErrors)
{
Console.WriteLine($"{error.PropertyPath}: {string.Join(", ", error.ErrorMessages)}");
}
}
Exception Mode (Throws on Failure)
var client = chatClient.AsStructuredChatClient()
.WithMaxRetries(3)
.WithSchemaValidationException(); // Throws SchemaValidationException
try
{
var result = await client.AskStructuredAsync<Product>("Extract product info");
// Use result.Data safely - guaranteed to be valid
}
catch (SchemaValidationException ex)
{
Console.WriteLine($"Validation failed after {ex.ValidationErrors.Count} errors");
foreach (var error in ex.ValidationErrors)
{
Console.WriteLine($"{error.PropertyPath}: {string.Join(", ", error.ErrorMessages)}");
}
}
Retry Behavior
The retry mechanism sends detailed validation errors back to the AI model, allowing it to learn from mistakes and provide corrected responses.
Ask Methods Reference
1. AskYesNoAsync
Extract boolean answers from natural language questions.
Signature:
Task<YesNoResult> AskYesNoAsync(string question)
Parameters:
question(string): The yes/no question to ask
Returns: YesNoResult
Answer(bool): True for "yes", False for "no"RawResponse(string): The raw "yes" or "no" responseHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Example:
var result = await client.AskYesNoAsync("Is the sky blue?");
Console.WriteLine($"Answer: {result.Answer}"); // True or False
Console.WriteLine($"Raw Response: {result.RawResponse}"); // "yes" or "no"
Console.WriteLine($"Is Valid: {result.IsValid}");
// More examples
var isRaining = await client.AskYesNoAsync("Based on the forecast, will it rain tomorrow?");
var isAffordable = await client.AskYesNoAsync("Is $50,000 an affordable price for a luxury car?");
2. AskSingleChoiceAsync
Select one option from a predefined list.
Signature:
Task<SingleChoiceResult<T>> AskSingleChoiceAsync<T>(
string question,
IEnumerable<T> choices
) where T : notnull
Parameters:
question(string): The question to askchoices(IEnumerable<T>): Available options to choose from
Returns: SingleChoiceResult<T>
Choice(T?): The selected choiceHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
// String choices
var colors = new[] { "Red", "Blue", "Green", "Yellow" };
var result = await client.AskSingleChoiceAsync(
"What color is the ocean?",
colors
);
Console.WriteLine($"Selected: {result.Choice}"); // "Blue"
// Numeric choices
var numbers = new[] { 1, 2, 3, 4, 5 };
var primeResult = await client.AskSingleChoiceAsync(
"Pick the smallest prime number",
numbers
);
Console.WriteLine($"Selected: {primeResult.Choice}"); // 2
// Custom object choices
var options = new[]
{
new { Id = 1, Name = "Basic" },
new { Id = 2, Name = "Premium" },
new { Id = 3, Name = "Enterprise" }
};
var planResult = await client.AskSingleChoiceAsync(
"Which plan offers the most features?",
options
);
3. AskMultipleChoiceAsync
Select multiple options from a list with optional min/max constraints.
Signature:
Task<MultipleChoiceResult<T>> AskMultipleChoiceAsync<T>(
string question,
IEnumerable<T> choices,
int? minSelections = null,
int? maxSelections = null
) where T : notnull
Parameters:
question(string): The question to askchoices(IEnumerable<T>): Available options to choose fromminSelections(int?, optional): Minimum number of selections requiredmaxSelections(int?, optional): Maximum number of selections allowed
Returns: MultipleChoiceResult<T>
Choices(List<T>): The selected choicesHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
// Without constraints
var fruits = new[] { "Apple", "Banana", "Orange", "Grape", "Mango" };
var result = await client.AskMultipleChoiceAsync(
"Which fruits are citrus?",
fruits
);
foreach (var choice in result.Choices)
{
Console.WriteLine($"- {choice}"); // Orange, possibly Mango
}
// With minimum selections
var languages = new[] { "C#", "Java", "Python", "JavaScript", "Go" };
var langResult = await client.AskMultipleChoiceAsync(
"Which languages run on the JVM?",
languages,
minSelections: 1
);
// With both constraints
var features = new[] { "Authentication", "Logging", "Caching", "Monitoring", "Queue" };
var featureResult = await client.AskMultipleChoiceAsync(
"Select essential features for a production API",
features,
minSelections: 2,
maxSelections: 4
);
4. AskEnumAsync
Extract enum values from responses.
Signature:
Task<EnumResult<TEnum>> AskEnumAsync<TEnum>(string question)
where TEnum : struct, Enum
Parameters:
question(string): The question to ask
Returns: EnumResult<TEnum>
Value(TEnum?): The extracted enum valueHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
public enum Priority
{
Low,
Medium,
High,
Critical
}
var result = await client.AskEnumAsync<Priority>(
"What priority should we assign to a security breach?"
);
Console.WriteLine($"Priority: {result.Value}"); // Priority.Critical
public enum DayOfWeek
{
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
var dayResult = await client.AskEnumAsync<DayOfWeek>(
"What day comes after Wednesday?"
);
Console.WriteLine($"Day: {dayResult.Value}"); // DayOfWeek.Thursday
public enum LogLevel
{
Trace, Debug, Information, Warning, Error, Critical
}
var logResult = await client.AskEnumAsync<LogLevel>(
"What log level should we use for exceptions?"
);
5. AskSingleWordAsync
Extract exactly one word (no whitespace allowed).
Signature:
Task<SingleWordResult> AskSingleWordAsync(string question)
Parameters:
question(string): The question to ask
Returns: SingleWordResult
Word(string?): The extracted single wordHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
var result = await client.AskSingleWordAsync(
"What is the capital of France?"
);
Console.WriteLine($"Word: {result.Word}"); // "Paris"
var colorResult = await client.AskSingleWordAsync(
"What color do you get when you mix blue and yellow?"
);
Console.WriteLine($"Color: {colorResult.Word}"); // "Green"
var verbResult = await client.AskSingleWordAsync(
"What is the past tense of 'run'?"
);
Console.WriteLine($"Verb: {verbResult.Word}"); // "Ran"
6. AskWithRegexPatternAsync
Extract data matching a specific regex pattern.
Signature:
Task<RegexResult> AskWithRegexPatternAsync(
string question,
string regexPattern,
string patternDescription = ""
)
Parameters:
question(string): The question or text to extract fromregexPattern(string): Regular expression pattern to matchpatternDescription(string, optional): Human-readable description of the pattern format
Returns: RegexResult
Value(string): The extracted value matching the patternHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
// Extract email address
var emailResult = await client.AskWithRegexPatternAsync(
"What is the support email? Contact us at support@example.com",
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
"Valid email address format"
);
Console.WriteLine($"Email: {emailResult.Value}"); // "support@example.com"
// Extract phone number
var phoneResult = await client.AskWithRegexPatternAsync(
"Call us at (555) 123-4567",
@"^\(\d{3}\) \d{3}-\d{4}$",
"US phone number format (XXX) XXX-XXXX"
);
Console.WriteLine($"Phone: {phoneResult.Value}"); // "(555) 123-4567"
// Extract ISBN
var isbnResult = await client.AskWithRegexPatternAsync(
"Book ISBN: 978-0-123456-78-9",
@"^\d{3}-\d{1}-\d{6}-\d{2}-\d{1}$",
"ISBN-13 format"
);
Console.WriteLine($"ISBN: {isbnResult.Value}"); // "978-0-123456-78-9"
// Extract hex color code
var colorResult = await client.AskWithRegexPatternAsync(
"The primary color is #FF5733",
@"^#[0-9A-Fa-f]{6}$",
"Hex color code"
);
Console.WriteLine($"Color: {colorResult.Value}"); // "#FF5733"
// Extract version number
var versionResult = await client.AskWithRegexPatternAsync(
"Current version: v2.3.1",
@"^v?\d+\.\d+\.\d+$",
"Semantic version format"
);
7. AskNumberAsync
Extract numeric values with optional min/max constraints.
Signature:
Task<NumberResult> AskNumberAsync(
string question,
double? minimum = null,
double? maximum = null
)
Parameters:
question(string): The question to askminimum(double?, optional): Minimum allowed valuemaximum(double?, optional): Maximum allowed value
Returns: NumberResult
Value(double?): The extracted numberHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
// Without constraints
var result = await client.AskNumberAsync(
"How many days are in a week?"
);
Console.WriteLine($"Number: {result.Value}"); // 7
// With minimum
var ageResult = await client.AskNumberAsync(
"What is the minimum voting age in the US?",
minimum: 0
);
Console.WriteLine($"Age: {ageResult.Value}"); // 18
// With maximum
var percentResult = await client.AskNumberAsync(
"What percentage of Earth is covered by water?",
maximum: 100
);
Console.WriteLine($"Percent: {percentResult.Value}"); // ~71
// With both constraints
var scoreResult = await client.AskNumberAsync(
"Rate this product on a scale of 1-10",
minimum: 1,
maximum: 10
);
Console.WriteLine($"Score: {scoreResult.Value}"); // e.g., 8.5
// Decimal values
var priceResult = await client.AskNumberAsync(
"What is the average price of coffee in USD?",
minimum: 0,
maximum: 20
);
Console.WriteLine($"Price: ${priceResult.Value}"); // e.g., 4.50
8. AskNumberRangeAsync
Extract a range of numbers (minimum and maximum values).
Signature:
Task<NumberRangeResult> AskNumberRangeAsync(
string question,
double? absoluteMinimum = null,
double? absoluteMaximum = null
)
Parameters:
question(string): The question to askabsoluteMinimum(double?, optional): Absolute minimum constraint for both valuesabsoluteMaximum(double?, optional): Absolute maximum constraint for both values
Returns: NumberRangeResult
Minimum(double?): The lower bound of the rangeMaximum(double?): The upper bound of the rangeHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
// Without constraints
var result = await client.AskNumberRangeAsync(
"What is the normal human body temperature range in Celsius?"
);
Console.WriteLine($"Range: {result.Minimum}°C - {result.Maximum}°C");
// e.g., 36.5 - 37.5
// With absolute constraints
var salaryResult = await client.AskNumberRangeAsync(
"What is the typical salary range for a software engineer in USD?",
absoluteMinimum: 0,
absoluteMaximum: 1000000
);
Console.WriteLine($"Salary Range: ${salaryResult.Minimum} - ${salaryResult.Maximum}");
// e.g., 70000 - 150000
// Temperature range
var tempResult = await client.AskNumberRangeAsync(
"What is the ideal temperature range for wine storage in Fahrenheit?",
absoluteMinimum: 0,
absoluteMaximum: 100
);
// Age range
var ageRangeResult = await client.AskNumberRangeAsync(
"What age range is considered 'middle age'?",
absoluteMinimum: 0,
absoluteMaximum: 120
);
9. AskDateTimeAsync
Extract dates and times in ISO 8601 format.
Signature:
Task<DateTimeResult> AskDateTimeAsync(string question)
Parameters:
question(string): The question to ask
Returns: DateTimeResult
DateTime(DateTime?): The extracted date/timeHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
var result = await client.AskDateTimeAsync(
"When did World War II end in Europe?"
);
Console.WriteLine($"DateTime: {result.DateTime}");
// 1945-05-08T00:00:00
var moonResult = await client.AskDateTimeAsync(
"When did humans first land on the moon?"
);
Console.WriteLine($"Moon Landing: {moonResult.DateTime:yyyy-MM-dd}");
// 1969-07-20
var independenceResult = await client.AskDateTimeAsync(
"When did the United States declare independence?"
);
Console.WriteLine($"Independence: {independenceResult.DateTime}");
// 1776-07-04T00:00:00
// With specific times
var launchResult = await client.AskDateTimeAsync(
"When did Apollo 11 launch? (Include time)"
);
// 1969-07-16T13:32:00Z
10. AskDateTimeRangeAsync
Extract a date/time range (start and end).
Signature:
Task<DateTimeRangeResult> AskDateTimeRangeAsync(string question)
Parameters:
question(string): The question to ask
Returns: DateTimeRangeResult
StartDate(DateTime?): The start date/timeEndDate(DateTime?): The end date/timeHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
var result = await client.AskDateTimeRangeAsync(
"What was the duration of the Renaissance period?"
);
Console.WriteLine($"Start: {result.StartDate}"); // ~1300-01-01
Console.WriteLine($"End: {result.EndDate}"); // ~1600-01-01
var warResult = await client.AskDateTimeRangeAsync(
"What were the start and end dates of World War I?"
);
Console.WriteLine($"WWI: {warResult.StartDate:yyyy-MM-dd} to {warResult.EndDate:yyyy-MM-dd}");
// 1914-07-28 to 1918-11-11
var projectResult = await client.AskDateTimeRangeAsync(
"The project runs from January 15, 2024 to March 30, 2024. What are the dates?"
);
var vacationResult = await client.AskDateTimeRangeAsync(
"Extract vacation dates: We're traveling from Dec 20, 2024 through Jan 5, 2025"
);
11. AskListAsync (Strings)
Extract a list of strings with optional size constraints.
Signature:
Task<ListResult<string>> AskListAsync(
string question,
int? minItems = null,
int? maxItems = null
)
Parameters:
question(string): The question to askminItems(int?, optional): Minimum number of items requiredmaxItems(int?, optional): Maximum number of items allowed
Returns: ListResult<string>
Items(List<string>): The extracted list of stringsHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
// Without constraints
var result = await client.AskListAsync(
"List the planets in our solar system"
);
foreach (var planet in result.Items)
{
Console.WriteLine($"- {planet}");
}
// Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
// With minimum items
var colorsResult = await client.AskListAsync(
"List primary colors",
minItems: 3
);
// Red, Blue, Yellow
// With maximum items
var topResult = await client.AskListAsync(
"List popular programming languages",
maxItems: 5
);
// With both constraints
var featuresResult = await client.AskListAsync(
"List essential features for a web application",
minItems: 3,
maxItems: 7
);
12. AskListAsync (Generic)
Extract a list of complex objects.
Signature:
Task<ListResult<T>> AskListAsync<T>(
string question,
int? minItems = null,
int? maxItems = null
)
Parameters:
question(string): The question to askminItems(int?, optional): Minimum number of items requiredmaxItems(int?, optional): Maximum number of items allowed
Returns: ListResult<T>
Items(List<T>): The extracted list of objectsHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
}
var result = await client.AskListAsync<Book>(
"List three classic novels with their authors and publication years",
minItems: 3,
maxItems: 3
);
foreach (var book in result.Items)
{
Console.WriteLine($"{book.Title} by {book.Author} ({book.Year})");
}
// To Kill a Mockingbird by Harper Lee (1960)
// 1984 by George Orwell (1949)
// Pride and Prejudice by Jane Austen (1813)
public class City
{
public string Name { get; set; }
public string Country { get; set; }
public int Population { get; set; }
}
var citiesResult = await client.AskListAsync<City>(
"List the 5 most populous cities in the world",
maxItems: 5
);
public class Recipe
{
public string Name { get; set; }
public List<string> Ingredients { get; set; }
public int PrepTimeMinutes { get; set; }
}
var recipesResult = await client.AskListAsync<Recipe>(
"Suggest 3 quick breakfast recipes",
minItems: 3,
maxItems: 3
);
13. AskStructuredAsync (Auto Schema)
Extract complex structured data using custom classes. The schema is automatically generated from the class definition.
Signature:
Task<StructuredResult<T>> AskStructuredAsync<T>(string question)
where T : class
Parameters:
question(string): The question or text to extract from
Returns: StructuredResult<T>
Data(T?): The extracted structured dataHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
// Basic example
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
public List<string> Hobbies { get; set; }
}
var result = await client.AskStructuredAsync<Person>(
@"Extract information about John:
John Doe is 30 years old.
His email is john.doe@example.com.
He enjoys reading, hiking, and photography."
);
var person = result.Data;
Console.WriteLine($"Name: {person.Name}");
Console.WriteLine($"Age: {person.Age}");
Console.WriteLine($"Email: {person.Email}");
Console.WriteLine($"Hobbies: {string.Join(", ", person.Hobbies)}");
// Complex nested example
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
public class Company
{
public string Name { get; set; }
public string Industry { get; set; }
public int EmployeeCount { get; set; }
public Address HeadquartersAddress { get; set; }
public List<string> Products { get; set; }
}
var companyResult = await client.AskStructuredAsync<Company>(
@"Extract company info:
TechCorp is a software company with 500 employees.
They are located at 123 Main St, San Francisco, CA 94105.
Their main products are CloudService, DataAnalyzer, and SecuritySuite."
);
14. AskStructuredAsync (Custom Schema)
Extract structured data using a custom JsonSchema.Net schema.
Signature:
Task<StructuredResult<T>> AskStructuredAsync<T>(
string question,
JsonSchema customSchema
) where T : class
Parameters:
question(string): The question or text to extract fromcustomSchema(JsonSchema): Custom JSON Schema for validation
Returns: StructuredResult<T>
Data(T?): The extracted structured dataHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
using Json.Schema;
// Custom schema with validation rules
var customSchema = new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("productName", new JsonSchemaBuilder()
.Type(SchemaValueType.String)
.MinLength(3)),
("price", new JsonSchemaBuilder()
.Type(SchemaValueType.Number)
.Minimum(0)
.Maximum(10000)),
("inStock", new JsonSchemaBuilder()
.Type(SchemaValueType.Boolean))
)
.Required("productName", "price")
.AdditionalProperties(false)
.Build();
public class Product
{
public string ProductName { get; set; }
public double Price { get; set; }
public bool InStock { get; set; }
}
var result = await client.AskStructuredAsync<Product>(
"Extract: The laptop costs $999 and is currently in stock",
customSchema
);
Console.WriteLine($"{result.Data.ProductName}: ${result.Data.Price}");
Console.WriteLine($"In Stock: {result.Data.InStock}");
// Complex custom schema with patterns
var emailSchema = new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("email", new JsonSchemaBuilder()
.Type(SchemaValueType.String)
.Pattern(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")),
("domain", new JsonSchemaBuilder()
.Type(SchemaValueType.String))
)
.Required("email", "domain")
.Build();
public class EmailInfo
{
public string Email { get; set; }
public string Domain { get; set; }
}
var emailResult = await client.AskStructuredAsync<EmailInfo>(
"Extract email: contact@example.com",
emailSchema
);
Working with JsonSchema.Net Attributes
StructuredChat fully supports JsonSchema.Net.Generation attributes for fine-grained schema control.
Available Attributes
using Json.Schema.Generation;
public class AdvancedPerson
{
[Required] // Field is required
[MinLength(2)]
[MaxLength(100)]
public string Name { get; set; }
[Minimum(0)]
[Maximum(150)]
public int Age { get; set; }
[Pattern(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
public string Email { get; set; }
[MinItems(1)]
[MaxItems(10)]
[UniqueItems(true)]
public List<string> Skills { get; set; }
[Description("Person's biography or description")]
[MinLength(10)]
public string Bio { get; set; }
[Title("Contact Number")]
[Pattern(@"^\+?[1-9]\d{1,14}$")]
public string PhoneNumber { get; set; }
}
var result = await client.AskStructuredAsync<AdvancedPerson>(
"Extract: John Smith, 35 years old, email john@example.com..."
);
Common JsonSchema.Net Attributes
| Attribute | Applies To | Description |
|---|---|---|
[Required] |
Properties | Marks property as required |
[Description("...")] |
All | Adds description for AI context |
[Title("...")] |
All | Sets display title |
[MinLength(n)] |
String | Minimum string length |
[MaxLength(n)] |
String | Maximum string length |
[Pattern("regex")] |
String | Regex pattern validation |
[Minimum(n)] |
Number | Minimum numeric value |
[Maximum(n)] |
Number | Maximum numeric value |
[ExclusiveMinimum(n)] |
Number | Exclusive minimum value |
[ExclusiveMaximum(n)] |
Number | Exclusive maximum value |
[MultipleOf(n)] |
Number | Value must be multiple of n |
[MinItems(n)] |
Array | Minimum array length |
[MaxItems(n)] |
Array | Maximum array length |
[UniqueItems(true)] |
Array | Array items must be unique |
[MinProperties(n)] |
Object | Minimum number of properties |
[MaxProperties(n)] |
Object | Maximum number of properties |
15. AskStructuredAsync (Schema as JSON String)
Extract structured data using a JSON schema provided as a string.
Signature:
Task<StructuredResult<T>> AskStructuredAsync<T>(
string question,
string schemaJson
) where T : class
Parameters:
question(string): The question or text to extract fromschemaJson(string): JSON schema as a string to validate against
Returns: StructuredResult<T>
Data(T?): The extracted structured dataHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
public class Product
{
public string Name { get; set; }
public double Price { get; set; }
public bool Available { get; set; }
}
// Define schema as JSON string
var schemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 3
},
"price": {
"type": "number",
"minimum": 0,
"maximum": 10000
},
"available": {
"type": "boolean"
}
},
"required": ["name", "price"],
"additionalProperties": false
}
""";
var result = await client.AskStructuredAsync<Product>(
"Extract: Premium Laptop costs $1299 and is available",
schemaJson
);
Console.WriteLine($"{result.Data.Name}: ${result.Data.Price}");
Console.WriteLine($"Available: {result.Data.Available}");
// Complex nested schema example
var orderSchemaJson = """
{
"type": "object",
"properties": {
"orderId": {
"type": "string",
"pattern": "^ORD-[0-9]{6}$"
},
"customer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": {
"type": "string",
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
}
},
"required": ["name", "email"]
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"product": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 }
}
},
"minItems": 1
}
},
"required": ["orderId", "customer", "items"]
}
""";
public class OrderItem
{
public string Product { get; set; }
public int Quantity { get; set; }
}
public class Customer
{
public string Name { get; set; }
public string Email { get; set; }
}
public class Order
{
public string OrderId { get; set; }
public Customer Customer { get; set; }
public List<OrderItem> Items { get; set; }
}
var orderResult = await client.AskStructuredAsync<Order>(
"Extract order: ORD-123456 for John Doe (john@example.com), items: 2x Laptop, 1x Mouse",
orderSchemaJson
);
Console.WriteLine($"Order ID: {orderResult.Data.OrderId}");
Console.WriteLine($"Customer: {orderResult.Data.Customer.Name} ({orderResult.Data.Customer.Email})");
foreach (var item in orderResult.Data.Items)
{
Console.WriteLine($" - {item.Quantity}x {item.Product}");
}
16. AskStructuredAsync (Schema as Anonymous Object)
Extract structured data using a JSON schema provided as an anonymous object.
Signature:
Task<StructuredResult<T>> AskStructuredAsync<T>(
string question,
object schemaObject
) where T : class
Parameters:
question(string): The question or text to extract fromschemaObject(object): JSON schema as an anonymous object to validate against
Returns: StructuredResult<T>
Data(T?): The extracted structured dataHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
// Define schema using anonymous object
var schemaObject = new
{
type = "object",
properties = new
{
name = new
{
type = "string",
minLength = 2,
maxLength = 100
},
age = new
{
type = "integer",
minimum = 0,
maximum = 150
},
email = new
{
type = "string",
pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
}
},
required = new[] { "name", "age", "email" },
additionalProperties = false
};
var result = await client.AskStructuredAsync<Person>(
"Extract: Alice Smith, 28 years old, email: alice@example.com",
schemaObject
);
Console.WriteLine($"{result.Data.Name}, {result.Data.Age}, {result.Data.Email}");
// More complex example with nested properties
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
public class Employee
{
public string EmployeeId { get; set; }
public string FullName { get; set; }
public string Department { get; set; }
public Address Address { get; set; }
}
var employeeSchema = new
{
type = "object",
properties = new
{
employeeId = new
{
type = "string",
pattern = "^EMP-[0-9]{4}$"
},
fullName = new { type = "string" },
department = new
{
type = "string",
@enum = new[] { "Engineering", "Sales", "HR", "Marketing" }
},
address = new
{
type = "object",
properties = new
{
street = new { type = "string" },
city = new { type = "string" },
zipCode = new
{
type = "string",
pattern = "^[0-9]{5}$"
}
},
required = new[] { "street", "city", "zipCode" }
}
},
required = new[] { "employeeId", "fullName", "department" },
additionalProperties = false
};
var empResult = await client.AskStructuredAsync<Employee>(
"Extract: EMP-1234, Bob Johnson, Engineering dept, 123 Main St, Boston, 02101",
employeeSchema
);
Console.WriteLine($"Employee: {empResult.Data.FullName} ({empResult.Data.EmployeeId})");
Console.WriteLine($"Department: {empResult.Data.Department}");
Console.WriteLine($"Address: {empResult.Data.Address.Street}, {empResult.Data.Address.City} {empResult.Data.Address.ZipCode}");
17. AskStructuredAsync (Schema as Dictionary)
Extract structured data using a JSON schema provided as a dictionary.
Signature:
Task<StructuredResult<T>> AskStructuredAsync<T>(
string question,
Dictionary<string, object> schemaDictionary
) where T : class
Parameters:
question(string): The question or text to extract fromschemaDictionary(Dictionary<string, object>): JSON schema as a dictionary to validate against
Returns: StructuredResult<T>
Data(T?): The extracted structured dataHasValue(bool): True if an answer was receivedIsValid(bool): Schema validation statusValidationErrors(List<SchemaValidationError>): Validation errors if any
Examples:
public class Book
{
public string Isbn { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public int YearPublished { get; set; }
public List<string> Genres { get; set; }
}
// Define schema using Dictionary
var schemaDictionary = new Dictionary<string, object>
{
["type"] = "object",
["properties"] = new Dictionary<string, object>
{
["isbn"] = new Dictionary<string, object>
{
["type"] = "string",
["pattern"] = "^[0-9]{3}-[0-9]{10}$"
},
["title"] = new Dictionary<string, object>
{
["type"] = "string",
["minLength"] = 1
},
["author"] = new Dictionary<string, object>
{
["type"] = "string"
},
["yearPublished"] = new Dictionary<string, object>
{
["type"] = "integer",
["minimum"] = 1000,
["maximum"] = 2100
},
["genres"] = new Dictionary<string, object>
{
["type"] = "array",
["items"] = new Dictionary<string, object>
{
["type"] = "string"
},
["minItems"] = 1,
["maxItems"] = 5
}
},
["required"] = new[] { "isbn", "title", "author", "yearPublished" },
["additionalProperties"] = false
};
var result = await client.AskStructuredAsync<Book>(
"Extract: ISBN 978-0123456789, '1984' by George Orwell, published 1949, genres: dystopian, political fiction",
schemaDictionary
);
Console.WriteLine($"{result.Data.Title} by {result.Data.Author} ({result.Data.YearPublished})");
Console.WriteLine($"ISBN: {result.Data.Isbn}");
Console.WriteLine($"Genres: {string.Join(", ", result.Data.Genres)}");
// Dynamic schema building example
var schemaBuilder = new Dictionary<string, object>
{
["type"] = "object",
["properties"] = new Dictionary<string, object>()
};
var properties = (Dictionary<string, object>)schemaBuilder["properties"];
// Add properties dynamically
properties["productCode"] = new Dictionary<string, object>
{
["type"] = "string",
["pattern"] = "^PRD-[A-Z]{3}-[0-9]{3}$"
};
properties["price"] = new Dictionary<string, object>
{
["type"] = "number",
["minimum"] = 0.01,
["maximum"] = 99999.99
};
properties["inStock"] = new Dictionary<string, object>
{
["type"] = "boolean"
};
schemaBuilder["required"] = new[] { "productCode", "price" };
schemaBuilder["additionalProperties"] = false;
public class ProductInfo
{
public string ProductCode { get; set; }
public double Price { get; set; }
public bool InStock { get; set; }
}
var productResult = await client.AskStructuredAsync<ProductInfo>(
"Extract: Product PRD-ABC-123 costs $49.99 and is in stock",
schemaBuilder
);
Console.WriteLine($"Product: {productResult.Data.ProductCode}");
Console.WriteLine($"Price: ${productResult.Data.Price}");
Console.WriteLine($"In Stock: {productResult.Data.InStock}");
// Conditional schema building based on runtime conditions
bool requiresAdvancedValidation = true;
var conditionalSchema = new Dictionary<string, object>
{
["type"] = "object",
["properties"] = new Dictionary<string, object>
{
["username"] = new Dictionary<string, object>
{
["type"] = "string",
["minLength"] = requiresAdvancedValidation ? 8 : 3,
["pattern"] = requiresAdvancedValidation ? "^[a-zA-Z0-9_]+$" : "^[a-zA-Z0-9]+$"
},
["password"] = new Dictionary<string, object>
{
["type"] = "string",
["minLength"] = requiresAdvancedValidation ? 12 : 6
}
},
["required"] = new[] { "username", "password" },
["additionalProperties"] = false
};
public class UserCredentials
{
public string Username { get; set; }
public string Password { get; set; }
}
var userResult = await client.AskStructuredAsync<UserCredentials>(
"Extract: username is john_doe123 and password is SecurePass2024!",
conditionalSchema
);
Schema Method Comparison
Here's a quick comparison to help you choose the right schema method:
| Method | Best For | Pros | Cons |
|---|---|---|---|
| Auto Schema | Simple POCOs with attributes | - Zero configuration<br>- Type-safe<br>- Uses C# attributes | - Less flexible<br>- Requires recompilation for changes |
| JsonSchema Object | Complex validation rules | - Full JsonSchema.Net features<br>- Type-safe schema building | - More verbose<br>- Requires JsonSchema.Net knowledge |
| JSON String | External schema files | - Easy to store/version<br>- Can load from config | - String manipulation<br>- No compile-time validation |
| Anonymous Object | Quick inline schemas | - Clean syntax<br>- Easy to read | - No IntelliSense<br>- Runtime errors only |
| Dictionary | Runtime schema generation | - Fully dynamic<br>- Conditional logic | - More verbose<br>- Type-unsafe |
Use Cases:
- JSON String: Best when you have an existing JSON schema file or need to store schemas as configuration
- Anonymous Object: Most readable for simple, inline schema definitions in code
- Dictionary: Ideal for dynamically building schemas at runtime based on application logic
Validation Results
All methods return result objects that include validation information:
public class BaseResult
{
public bool HasValue { get; }
public bool IsValid { get; }
public List<SchemaValidationError> ValidationErrors { get; }
}
// Check validation status
var result = await client.AskStructuredAsync<Product>("...");
if (result.IsValid)
{
// Use result.Data
}
else
{
foreach (var error in result.ValidationErrors)
{
Console.WriteLine($"Error at {error.PropertyPath}:");
foreach (var message in error.ErrorMessages)
{
Console.WriteLine($" - {message}");
}
}
}
Understanding HasValue vs IsValid
HasValue: Checks if the response contains actual datatrue= Data exists (not null, not empty, not default value)false= No data received or empty response
IsValid: Checks if the data passes schema validationtrue= Data conforms to all schema rules and constraintsfalse= Data violates one or more schema requirements
ValidationErrors: Detailed error information when validation fails- Empty when
IsValidistrue - Contains specific error messages and property paths when
IsValidisfalse
- Empty when
Contributing
We welcome contributions to make StructuredChat even better! Here are some ways you can help:
🌟 Star this repository if you find it useful!
Your star helps others discover this library and motivates continued development.
🔧 Pull Requests Welcome
We're open to pull requests!
Please feel free to fork the repository and submit a pull request. For larger changes, consider opening an issue first to discuss your approach.
📝 Reporting Issues
Found a bug or have a suggestion? Please open an issue with:
- A clear description of the problem or enhancement
- Steps to reproduce (for bugs)
- Sample code demonstrating the issue
- Expected vs actual behaviour
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- JsonSchema.Net (>= 7.4.0)
- JsonSchema.Net.Generation (>= 5.1.1)
- Microsoft.Extensions.AI.Abstractions (>= 10.0.0)
- System.Text.Json (>= 10.0.0)
-
net10.0
- JsonSchema.Net (>= 7.4.0)
- JsonSchema.Net.Generation (>= 5.1.1)
- Microsoft.Extensions.AI.Abstractions (>= 10.0.0)
-
net8.0
- JsonSchema.Net (>= 7.4.0)
- JsonSchema.Net.Generation (>= 5.1.1)
- Microsoft.Extensions.AI.Abstractions (>= 10.0.0)
-
net9.0
- JsonSchema.Net (>= 7.4.0)
- JsonSchema.Net.Generation (>= 5.1.1)
- Microsoft.Extensions.AI.Abstractions (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.