# AiStructuredDataView
## Description
BitAiStructuredDataView renders a structured output part of a message: the JSON value a model produced instead of prose. A renderer the app registered for the part's schema name draws it when there is one, inside an error boundary; otherwise the view reads the shape of the value - an array of objects becomes a table, an object a list of labelled values, anything else a collapsible tree. The value is re-parsed leniently on every delta, so all of them fill in while the model is still writing, and a part that has received nothing yet shows a thinking indicator.
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Classes | `BitAiStructuredDataViewClassStyles?` | null | Custom CSS classes for different parts of the structured data view. |
| Message | `BitAiMessage?` | null | The message the part belongs to, handed to a renderer registered for the part's schema (in its BitAiStructuredDataContext) so it can read the rest of the answer. A new, empty assistant message stands in while it is null. The default rendering does not read it. |
| Part | `BitAiStructuredDataPart` | null | The structured part to render (required). Its SchemaName picks a renderer registered with RegisterStructured; without one, its Value is drawn by its shape - an array of objects as a BitAiDataTable, an object as a list of labelled values, anything else as a BitAiJsonView. While nothing parseable has arrived, a streaming part shows a BitAiThinkingIndicator and a finished one nothing. |
| Styles | `BitAiStructuredDataViewClassStyles?` | null | Custom CSS styles for different parts of the structured data view. |
| AriaLabel | `string?` | null | Gets or sets the accessible label for the component, used by assistive technologies. |
| Class | `string?` | null | Gets or sets the CSS class name(s) to apply to the rendered element. |
| Dir | `BitDir?` | null | Gets or sets the text directionality for the component's content. |
| Disabled | `bool` | false | Gets or sets a value indicating whether the component is disabled and cannot respond to user interaction. |
| ForceAnimation | `bool` | false | Gets or sets a value indicating whether the component's animations play at their full duration even when reduced motion is requested. |
| HtmlAttributes | `Dictionary<string, object>` | new Dictionary<string, object>() | Captures additional HTML attributes to be applied to the rendered element, in addition to the component's parameters. An attribute written on the component wins over an entry of the same name. |
| Id | `string?` | null | Gets or sets the unique identifier for the component's root element. |
| Style | `string?` | null | Gets or sets the CSS style string to apply to the rendered element. |
| TabIndex | `string?` | null | Gets or sets the tab order index for the component when navigating with the keyboard. |
| Visibility | `BitVisibility` | BitVisibility.Visible | Gets or sets the visibility state (visible, hidden, or collapsed) of the component. |
## Public Members
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| UniqueId | `Guid` | Guid.NewGuid() | Gets the readonly unique identifier for the component's root element, assigned when the component instance is constructed. |
| RootElement | `ElementReference` | | Gets the reference to the root HTML element associated with this component. |
## Enums
### BitVisibility Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Visible | 0 | The content of the component is visible. |
| Hidden | 1 | The content of the component is hidden, but the space it takes on the page remains (visibility:hidden). |
| Collapsed | 2 | The component is hidden (display:none). |
### BitDir Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Ltr | 0 | Ltr (left to right) is to be used for languages that are written from the left to the right (like English). |
| Rtl | 1 | Rtl (right to left) is to be used for languages that are written from the right to the left (like Arabic). |
| Auto | 2 | Auto lets the user agent decide. It uses a basic algorithm as it parses the characters inside the element until it finds a character with a strong directionality, then applies that directionality to the whole element. |
## Sub Classes
### BitAiStructuredDataViewClassStyles Properties
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Root | `string?` | null | Custom CSS classes/styles for the root element of the BitAiStructuredDataView. |
| List | `string?` | null | Custom CSS classes/styles for the list of labelled values an object is shown as by the BitAiStructuredDataView. |
| Entry | `string?` | null | Custom CSS classes/styles for each entry - a label and its value - of the list of the BitAiStructuredDataView. |
| Label | `string?` | null | Custom CSS classes/styles for the label of each entry of the list of the BitAiStructuredDataView. |
| Value | `string?` | null | Custom CSS classes/styles for the value of each entry of the list of the BitAiStructuredDataView. |
| Empty | `string?` | null | Custom CSS classes/styles for the dash a missing value is shown as in the list of the BitAiStructuredDataView. |
### BitAiStructuredDataPart Properties
A part of a message holding the structured output of a model: raw JSON that streams in and a lenient parse of it. A session creates and fills one from the BitAiStructuredDataDeltaEvent and BitAiStructuredDataEvent of its transport; an app creates one to show a value it has itself. Safe to read on the renderer's thread while it is appended to on the transport's.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| new(schemaName?, partId?) | `constructor` | | Creates an empty part that is streaming: Append the JSON as it arrives, then Complete it. |
| new(value, schemaName?, partId?) | `constructor` | | Creates a finished part holding a value. |
| SchemaName | `string?` | null | A name identifying the shape of the value, which picks the renderer registered for it with BitAiRendererRegistry.RegisterStructured. |
| Value | `JsonElement?` | null | The parsed value once complete; until then a lenient parse of the JSON received so far (BitAiPartialJson.TryParse, described on the AiJsonView page, /components/ai/jsonview#partial-json), null while nothing parseable has arrived. |
| RawJson | `string` | "" | The raw JSON text received so far. |
| IsStreaming | `bool` | true | Whether more JSON may still arrive: true for a part created empty until Complete is called, false for one created with a value. |
| Append(delta) | `void` | | Appends raw JSON text; Value is re-parsed the next time it is read. |
| Complete(value?) | `void` | | Marks the part complete, with the value given or the accumulated text parsed strictly (falling back to the lenient parse when it is not valid JSON). |
| As<T>(typeInfo) | `T?` | | Deserializes the current value - partial or not - into a typed model with the metadata of the app's own JsonSerializerContext, safe to trim. Give the model nullable members: the fields not written yet are absent. Null when nothing parseable has arrived or the value does not fit the type. |
| As<T>(options?) | `T?` | | The same with the reflection-based serializer (the web defaults when no options are given), marked as unsafe to trim and for native AOT. |
### BitAiRendererRegistry Properties
Where an app registers its own renderers, which the components resolve before their built-in views. A singleton: register at startup (AddBitBlazorUIAIServices(ai => ai.ConfigureRenderers(renderers => ...))) and treat the entries as fixed afterwards - every entry is app-wide. A region with renderers of its own cascades a registry of its own (new BitAiRendererRegistry(), in a CascadingValue), which every AI component inside it uses in place of the registered one. These are its structured-data members; the whole registry is on the AiMessagePartView page (/components/ai/messagepartview#renderer-registry).
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| RegisterStructured(schemaName, renderer) | `void` | | Registers a BitAiStructuredDataRenderer for a schema name (case does not matter): every BitAiStructuredDataView of a part with that SchemaName renders it in place of the table, list or tree it would draw, inside a BitAiRendererBoundary; a later registration replaces an earlier one. A part with no SchemaName is never drawn by one. |
| FindStructured(schemaName) | `BitAiStructuredDataRenderer?` | | The renderer registered for a schema name, or null (also for a null name) when the view reads the shape of the value itself. |
### BitAiStructuredDataRenderer Properties
The delegate a custom structured-data renderer is: public delegate RenderFragment BitAiStructuredDataRenderer(BitAiStructuredDataContext context). It is called each time the view's parameters are set - on every render of a streaming answer - so it reads what it draws off the part as it renders; a part still streaming holds a partial Value (or none yet).
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| context | `BitAiStructuredDataContext` | | The part and the message it belongs to. |
| (return value) | `RenderFragment` | | The markup drawn in place of the built-in table, list or tree. |
### BitAiStructuredDataContext Properties
What a renderer registered with BitAiRendererRegistry.RegisterStructured(schemaName, context => ...) is handed (a sealed record), through ConfigureRenderers of AddBitBlazorUIAIServices.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Part | `BitAiStructuredDataPart` | | The part being rendered. |
| Message | `BitAiMessage` | | The message the part belongs to: the Message parameter of the view, or a new assistant message when it is not set. |
## CSS Variables
Public CSS custom properties read off the component's root. Set one on `:root` to restyle every instance, on any ancestor to restyle the ones inside it, or on the `Style` of one instance to restyle it and every instance nested inside it, which inherits the value too - a parameter changes one alone, as does the real CSS property on its `Style` / `Styles` when that property does not inherit (`opacity`); an inherited one (`color`, `font-family`) reaches the nested instances that do not set it themselves.
| Name | Default Value | Description |
| :--- | :------------ | :---------- |
| `--bit-AiStructuredDataView-gap` | spacing(1) | Room between two entries of the list of labelled values. |
| `--bit-AiStructuredDataView-padding` | spacing(2) | Inset of the list. |
| `--bit-AiStructuredDataView-background` | var(--bit-clr-bg-sec) | Background of the list. |
| `--bit-AiStructuredDataView-border-color` | var(--bit-clr-brd-pri) | Color of the frame around the list. |
| `--bit-AiStructuredDataView-radius` | var(--bit-shp-radius-surface) | Corner of the frame. |
| `--bit-AiStructuredDataView-label-color` | var(--bit-clr-fg-pri) | Color of the labels. |
| `--bit-AiStructuredDataView-label-font-size` | var(--bit-tpg-fs-sm) | Size of the labels. |
| `--bit-AiStructuredDataView-label-font-weight` | var(--bit-tpg-fw-medium) | Weight of the labels. |
| `--bit-AiStructuredDataView-value-color` | var(--bit-clr-fg-pri) | Color of the values. |
| `--bit-AiStructuredDataView-empty-color` | var(--bit-clr-fg-ter) | Color of the dash a missing value is shown as. |
## Examples
\n**Basic**:
```razor
```
```csharp
private readonly BitAiStructuredDataPart flightPart = new(JsonDocument.Parse("""
{ "flightNumber": "BA 117", "from": "LHR", "to": "JFK", "departure": "2026-11-02 09:40", "seats": 2, "refundable": false, "meal": null }
""").RootElement);
```
\n**Shapes**:
```razor
```
```csharp
private static JsonElement Parse(string json) => JsonDocument.Parse(json).RootElement;
private readonly BitAiStructuredDataPart ordersPart = new(Parse("""
[
{ "order": "ord_4821", "status": "shipped", "total": 129.5 },
{ "order": "ord_4822", "status": "pending", "total": 42 }
]
"""));
private readonly BitAiStructuredDataPart tagsPart = new(Parse("""["blazor", "webassembly", "streaming"]"""));
private readonly BitAiStructuredDataPart customerPart = new(Parse("""
{ "name": "Ada Lovelace", "address": { "city": "London", "zip": "N1 9GU" }, "orders": ["ord_4821", "ord_4822"] }
"""));
```
\n**Streaming**:
```razor
@implements IAsyncDisposable
Compare the options
@if (OptionsAnswer is { } answer)
{
@foreach (var part in answer.PartsOf())
{
}
}
```
```csharp
// A scripted transport that streams four rows of JSON a few characters at a time (see the next tab).
private readonly BitAiChatSession optionsSession = new(AiDemoContentTransports.Structured());
private BitAiRenderScheduler? renderScheduler;
protected override void OnInitialized()
{
// The session says so on every streamed delta; the scheduler turns any number of those into one render per 50 ms.
renderScheduler = new(() => InvokeAsync(StateHasChanged));
optionsSession.StateChanged += renderScheduler.Request;
}
private BitAiMessage? OptionsAnswer => optionsSession.Conversation.LastMessage is { Role.IsAssistant: true } last ? last : null;
private Task AskForOptionsAsync() => optionsSession.SubmitAsync("Compare the options");
public async ValueTask DisposeAsync()
{
if (renderScheduler is not null) await renderScheduler.DisposeAsync();
await optionsSession.DisposeAsync();
}
```
`AiDemoContentTransports.cs`:
```csharp
public static IBitAiTransport Structured() =>
new AiScriptedTransport((request, token) => AiScript.Stream(token, async yielder =>
{
await yielder(new BitAiMessageStartedEvent());
const string intro = "Four candidates, ranked by how much work each one would be.\n\n";
await foreach (var evt in AiScript.Write(intro, 4, 12, token)) await yielder(evt);
const string json = """
[
{"option":"Server-side rendering","effort":"Low","risk":"Low","notes":"Fastest to ship"},
{"option":"WebAssembly","effort":"Medium","risk":"Low","notes":"Best offline story"},
{"option":"Hybrid per page","effort":"Medium","risk":"Medium","notes":"Two mental models"},
{"option":"Rewrite the front end","effort":"High","risk":"High","notes":"Not this quarter"}
]
""";
// Fourteen characters at a time: the JSON is incomplete until the last slice.
foreach (var slice in AiScript.Slice(json, 14))
{
await AiScript.Pause(70, token);
await yielder(new BitAiStructuredDataDeltaEvent(slice) { SchemaName = "options" });
}
await yielder(new BitAiMessageCompletedEvent());
}));
```
\n**Registered renderers**:
```razor
@implements IAsyncDisposable
Book a table for Friday
@if (BookingAnswer is { } booking)
{
@foreach (var part in booking.PartsOf())
{
}
}
```
```csharp
// A scripted transport that streams the JSON Schema of a form, under the schema name booking_form (see the last tab).
private readonly BitAiChatSession bookingSession = new(AiDemoInteractiveTransports.ProposesForm());
private BitAiRenderScheduler? renderScheduler;
protected override void OnInitialized()
{
renderScheduler = new(() => InvokeAsync(StateHasChanged));
bookingSession.StateChanged += renderScheduler.Request;
}
private BitAiMessage? BookingAnswer => bookingSession.Conversation.LastMessage is { Role.IsAssistant: true } last ? last : null;
private Task ProposeBookingAsync() => bookingSession.SubmitAsync("Book a table for Friday");
public async ValueTask DisposeAsync()
{
if (renderScheduler is not null) await renderScheduler.DisposeAsync();
await bookingSession.DisposeAsync();
}
```
`Program.cs`:
```csharp
builder.Services.AddBitBlazorUIAIServices(ai =>
{
ai.UseTransport();
// A schema the model fills in becomes a form the user checks, instead of raw JSON.
ai.ConfigureRenderers(renderers => renderers.RegisterStructured("booking_form", context => builder =>
{
builder.OpenComponent(0);
builder.AddComponentParameter(1, nameof(BookingForm.Part), context.Part);
builder.CloseComponent();
}));
});
```
`BookingForm.razor`:
```razor
@* Not submitted before the whole schema has arrived, and not edited after it was. *@
@code {
private BitAiFormDefinition definition = new();
private bool submitted;
[Parameter, EditorRequired] public BitAiStructuredDataPart Part { get; set; } = default!;
[CascadingParameter(Name = "BookingSubmitted")]
public EventCallback> Submitted { get; set; }
// Rebuilt as the schema streams in; the form keeps whatever the user already typed.
protected override void OnParametersSet() =>
definition = Part.Value is { } schema ? BitAiFormDefinition.FromJsonSchema(schema) : new BitAiFormDefinition();
private async Task OnSubmitAsync(IReadOnlyDictionary values)
{
submitted = true;
await Submitted.InvokeAsync(values);
}
}
```
`AiDemoInteractiveTransports.cs`:
```csharp
public static IBitAiTransport ProposesForm() =>
new AiScriptedTransport((request, token) => AiScript.Stream(token, async yielder =>
{
await yielder(new BitAiMessageStartedEvent());
const string intro = "I can book that. Check the details first, since I inferred two of them.\n\n";
await foreach (var evt in AiScript.Write(intro, 4, 12, token)) await yielder(evt);
// The two details the model inferred go in as the defaults the form starts from: two guests, and the coming Friday.
var today = DateTime.Today;
var friday = today.AddDays(((int)DayOfWeek.Friday - (int)today.DayOfWeek + 7) % 7);
var date = friday.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var schema = $$"""
{
"title": "Book a table",
"type": "object",
"required": ["restaurant", "guests", "date"],
"properties": {
"restaurant": { "type": "string", "title": "Restaurant" },
"guests": { "type": "integer", "title": "Guests", "minimum": 1, "maximum": 12, "default": 2 },
"date": { "type": "string", "format": "date", "title": "Date", "default": "{{date}}" },
"seating": { "type": "string", "enum": ["Inside", "Terrace", "No preference"] },
"notes": { "type": "string", "format": "textarea", "title": "Notes for the restaurant" }
}
}
""";
// Named booking_form, which is what picks the registered renderer.
foreach (var slice in AiScript.Slice(schema, 30))
{
await AiScript.Pause(50, token);
await yielder(new BitAiStructuredDataDeltaEvent(slice) { SchemaName = "booking_form" });
}
await yielder(new BitAiMessageCompletedEvent());
}));
```
\n**Cascading parameters**:
```razor
```
```csharp
private readonly BitAiStructuredDataPart flightPart = new(JsonDocument.Parse("""
{ "flightNumber": "BA 117", "from": "LHR", "to": "JFK", "departure": "2026-11-02 09:40", "seats": 2, "refundable": false, "meal": null }
""").RootElement);
private readonly BitAiStructuredDataViewParams[] structuredDataViewParams =
[
new() { Styles = new() { List = "border-radius: 1rem", Label = "color: var(--bit-clr-pri)" } },
];
```
\n**Style & Class**:
```razor
```
```csharp
private readonly BitAiStructuredDataPart flightPart = new(JsonDocument.Parse("""
{ "flightNumber": "BA 117", "from": "LHR", "to": "JFK", "departure": "2026-11-02 09:40", "seats": 2, "refundable": false, "meal": null }
""").RootElement);
```
\n**RTL**:
```razor
```
```csharp
private readonly BitAiStructuredDataPart persianFlightPart = new(JsonDocument.Parse("""
{ "شماره پرواز": "IR 721", "مبدا": "تهران", "مقصد": "لندن", "صندلی": 2, "قابل استرداد": true, "غذا": null }
""").RootElement);
```