Skip to main content

BlazorDynamicForm

Automatically generate fully interactive Blazor forms from C# classes using type annotations — no hand-written markup required.

BlazorDynamicForm is an internal library that turns annotated C# model classes into complete, editable UI forms at runtime. It relies on two cooperating layers: the TypeAnnotationParser, which reflects over a class to produce a portable schema (SchemeModel), and the DynamicForm Blazor component, which reads that schema and renders the appropriate input controls. The schema can also be serialized to YAML for storage or external editing, then deserialized back at runtime.

Architecture overview

The system is split into two NuGet-style projects:

ProjectResponsibility
TypeAnnotationParserReflects a C# Type, walks its properties and attributes, and outputs a SchemeModel — a tree of SchemeProperty nodes that describes every field, its data type, nesting, and attached metadata attributes.
BlazorDynamicFormConsumes a SchemeModel and renders a Blazor component tree. Each PropertyType is mapped to a Razor component (e.g., StringComponent, IntComponent, ObjectComponent). Custom attributes can override the default renderer.
┌────────────────────┐        ┌─────────────────────┐        ┌──────────────┐
│ C# Class + Attrs │──parse─▶ SchemeModel (JSON/ │──bind──▶ <DynamicForm │
│ (or YAML config) │ │ YAML portable) │ │ Scheme=... │
└────────────────────┘ └─────────────────────┘ │ Data=...> │
└──────────────┘

TypeAnnotationParser

The parser uses .NET reflection to convert any Type into a SchemeModel.

Core types

TypeDescription
SchemeModelRoot of the schema tree. Extends SchemeProperty and adds a References dictionary for deduplicating complex/nested types.
SchemePropertyDescribes a single property: its Type (enum), optional Ref pointer, child Properties (for objects), Indices (for arrays/dictionaries), Enum values, and a list of Attributes.
PropertyTypeEnum — String, Integer, Float, Double, Decimal, Boolean, Enum, Object, Array, Dictionary, File.
AttributeSchemeBase class for all metadata attributes that the parser collects. Custom attributes must inherit from this.
ParserConfigurationConfiguration record passed to the parser constructor. Contains a list of Annotation entries.

How parsing works

  1. Instantiate the parser with a ParserConfiguration:
var config = new ParserConfiguration();
var parser = new TypeAnnotationParser(config);
  1. Call Parse<T>() (or Parse(Type)):
SchemeModel scheme = parser.Parse<MyClass>();
  1. The parser walks every public property of the type:
    • Determines the PropertyType using DeterminePropertyType().
    • Collects all attributes that derive from AttributeScheme via reflection.
    • For Object types, recursively processes child properties. Complex types are stored once in SchemeModel.References and referenced by key (#TypeName) to avoid duplication and infinite recursion.
    • For Array / Dictionary types, extracts the generic argument(s) and stores them in SchemeProperty.Indices.
    • A depth limit of 20 prevents infinite recursion on self-referencing types.

Type mapping rules

C# TypePropertyType
stringString
boolBoolean
int, long, short, byte (and unsigned variants)Integer
floatFloat
doubleDouble
decimalDecimal
Any enumEnum
T[], List<T>, any IListArray
Dictionary<K,V>, any IDictionaryDictionary
Any other class/structObject

Reference deduplication

When the parser encounters a complex Object type that has already been processed, it reuses the existing entry in SchemeModel.References instead of duplicating the definition. Properties that point to a shared type store a Ref string (e.g., #Cube) rather than inlining the full schema.

public class Parent
{
public Child A { get; set; } // both reference "#Child"
public Child B { get; set; }
}

YAML serialization

The SchemeModel can be round-tripped to and from YAML using the TypeAnnotationParser.Serialization.Scheme static class. This enables storing form definitions as configuration files that can be edited outside of code.

Serialize a type to YAML

using TypeAnnotationParser.Serialization;

List<Type> attributeTypes = new()
{
typeof(CodeEditorAttribute),
typeof(MultipleSelectAttribute),
typeof(NameAttribute),
typeof(GridAttribute),
typeof(BoxAttribute),
typeof(LabelAttribute)
};

string yaml = Scheme.GetYamlFromScheme<SwitchTask>(attributeTypes);

Each attribute type is registered as a YAML tag (!CodeEditor, !MultipleSelect, etc.) so that custom metadata survives the round-trip.

Deserialize YAML back to a scheme

SchemeModel? scheme = Scheme.GetSchemeFromYaml(attributeTypes, yaml);

Example YAML output

references:
'#SwitchCase':
name: SwitchCase
type: Object
properties:
Op:
type: String
attributes:
- !Label
name: Op
- !MultipleSelect
options:
- Equals
- Contains
- Regex
- '>'
- '<'
Value:
type: String
attributes:
- !Name
name: Value
Branch:
type: String
attributes:
- !Name
name: Branch
name: SwitchTask
type: Object
properties:
Variable:
type: String
attributes:
- !Name
name: Variable to evaluate
DefaultBranch:
type: String
attributes:
- !Name
name: Default branch
Cases:
type: Array
attributes:
- !Name
name: Cases
indices:
- ref: '#SwitchCase'

BlazorDynamicForm component library

Registration

Register the form services in Program.cs:

builder.Services.AddBlazorDynamicForm();

This registers a singleton DynamicFormConfiguration that maps every PropertyType to a default Blazor component and wires up custom attribute renderers.

Default renderer mappings

PropertyTypeComponentUI Control
StringStringComponentText input with icon
IntegerIntComponentNumeric input
FloatFloatComponentNumeric input
DoubleDecimalComponentNumeric input
DecimalDecimalComponentNumeric input
BooleanBooleanComponentToggle switch
EnumEnumComponentDropdown select
ObjectObjectComponentRecursive fieldset with grid layout
ArrayListComponentDynamic list with add/remove buttons
DictionaryDictionaryComponentKey-value editor with add/remove
FileFileComponentFile upload

Custom attribute renderers

Certain attributes override the default component for a property, regardless of its PropertyType:

AttributeComponentEffect
TextAreaAttributeTextAreaComponentRenders a multi-line textarea
CodeEditorAttributeCodeEditorComponentRenders a Monaco code editor
MultipleSelectAttributeMultipleOptionsComponentRenders a multi-select dropdown

Using the <DynamicForm> component

@using BlazorDynamicForm.Components
@using TypeAnnotationParser

<DynamicForm Scheme="@_scheme" Data="_formData" OnValidSubmit="@OnSubmit">
<SubmitTemplate>
<button type="submit" class="btn btn-primary">Save</button>
</SubmitTemplate>
</DynamicForm>

@code {
private SchemeModel _scheme;
private IDictionary<string, object> _formData = new Dictionary<string, object>();

protected override void OnParametersSet()
{
var parser = new TypeAnnotationParser(new ParserConfiguration());
_scheme = parser.Parse<MyConfigClass>();
}

void OnSubmit(IDictionary<string, object>? data)
{
// data contains the form values as a dictionary tree
var json = JsonConvert.SerializeObject(data, Formatting.Indented);
}
}

Parameters

ParameterTypeDescription
SchemeSchemeModelThe parsed schema that defines the form structure.
DataIDictionary<string, object>Two-way bound dictionary holding the form values. Created/validated automatically by ObjectGenerator.
OnValidSubmitEventCallback<IDictionary<string, object>?>Fired when the form is submitted and all validation rules pass.
OnDataChangedEventCallback<IDictionary<string, object>>Fired whenever any field value changes (debounced 300ms). Use for auto-save or live sync.
SubmitTemplateRenderFragmentCustom markup for the submit button area.

Attributes reference

All custom attributes inherit from AttributeScheme (metadata-only) or DynamicRendererComponent (overrides the rendered component).

Layout & display attributes

AttributeBasePropertiesDescription
GridAttributeAttributeSchemeSize (int, 1–12)Controls the Bootstrap column width. Default is 12 (full width).
LabelAttributeAttributeSchemeLabel (string), Position (None / Inline / Top)Adds a label above or beside the field.
BoxAttributeAttributeSchemeVisibility (None / Visible)Wraps the object in a collapsible card container.
NameAttributeAttributeSchemeName (string)Overrides the property name displayed in the UI.
PlaceholderAttributeAttributeSchemePlaceholder (string)Sets placeholder text on string inputs.
TooltipAttributeAttributeSchemeText (string)Shows a native browser tooltip on hover over the field label.
DocumentationAttributeAttributeSchemeDescription, DocumentationUrl, Example, ExampleLanguageAttaches an expandable documentation panel (book icon) next to the label. See Documentation attribute.
CsvImportAttributeAttributeSchemeLabelColumn, ValueColumn, LabelProperty, ListProperty, ValuePropertyOn a List<T> field, adds an "Import CSV" button that parses a long-format CSV client-side and replaces the rows. Grouped (ListProperty set) or flat. 5 MB cap.

Renderer-override attributes

AttributeBasePropertiesDescription
TextAreaAttributeDynamicRendererComponentForces a multi-line textarea renderer.
CodeEditorAttributeDynamicRendererComponentLanguage, Theme, Example, Templated, VariablesForces a Monaco code editor. Language sets syntax highlighting (e.g., "javascript", "csharp", "json"). Set Templated = true (or supply Variables) to register a Monaco completion provider that pops the workflow's declared variables on { trigger and inserts them as {name}.
MultipleSelectAttributeDynamicRendererComponentOptions (string[])Forces a multi-select dropdown with the given choices.
SliderAttributeDynamicRendererComponentStep (double, default 0.01)Renders a numeric field as a range slider paired with a numeric readout. Bounds come from [Range]; the bound property may be float / double / decimal / int / long.
SelectBoxAttributeDynamicRendererComponentOptions (string[])Forces a single-select dropdown (defined in TypeAnnotationParser).
DataProviderAttributeDynamicRendererComponentPath (string), ValueField (string), LabelField (string)Fetches selectable options from an HTTP endpoint. See Data Provider section.
EnvironmentPickerAttributeDynamicRendererComponentTwo-step cascading picker (project → env) for choosing a workspace environment. Binds a Guid? env id. See Environment Picker section.

Validation & value attributes

AttributeBasePropertiesDescription
RequiredRuleValidationRuleValidates the field is not null, empty, or whitespace. Evaluated on form submit.
RangeAttributeAttributeSchemeMin, MaxConstrains numeric values to a min/max range. Supports int, float, and decimal.
DefaultValueFormAttributeAttributeSchemeDefaultValue (object)Sets a default value when ObjectGenerator creates data for a null field.
ReadonlyFormAttributeAttributeSchemeMakes the field read-only. Applied to all input components (StringComponent, IntComponent, BooleanComponent, etc.).

ObjectGenerator — data creation & validation

The ObjectGenerator extension method CreateOrValidateData walks a SchemeModel and either creates a fresh data dictionary matching the schema or validates/repairs an existing one.

// Create empty data matching the schema
object data = scheme.CreateOrValidateData(scheme, null);

// Validate existing data against the schema
object validated = scheme.CreateOrValidateData(scheme, existingData);

Options

var options = new ObjectGeneratorOptions
{
MaxRecursiveDepth = 10, // prevent infinite recursion
InitStringsEmpty = true, // initialize strings as ""
CreateCollectionElement = false, // auto-add one item to empty lists
CreateDictionaryElement = false, // auto-add one entry to empty dicts
CreateObjectElement = false
};

Behavior per type

TypeBehavior
StringReturns existing string or "". If DefaultValueFormAttribute is present and data is null, uses that instead.
IntegerReturns existing int or 0
Float / Double / DecimalReturns existing value or 0
BooleanReturns existing bool or false
EnumReturns existing int index or 0
DateTimeReturns existing DateTime or DateTime.MinValue
ObjectEnsures a Dictionary<string, object?>, validates each sub-property, removes unknown keys
ArrayEnsures a List<object?>, optionally creates one element, validates existing items
DictionaryEnsures a Dictionary<string, object?>, validates values against the value-type schema

Validation

The form evaluates all ValidationRule attributes on submit. If any rule fails, the form displays error messages and does not fire OnValidSubmit.

How it works

  1. User clicks the submit button.
  2. DynamicForm recursively walks the entire SchemeModel tree — including nested objects, array items, and dictionary values.
  3. For each property/item, it collects all attributes that inherit from ValidationRule.
  4. Each rule's IsValid(SchemeModel, value) is called with the current field value.
  5. Failed rules produce error messages via FormatErrorMessage(displayName).
  6. Errors are displayed in a red alert box above the submit button.

Recursive validation scope

StructureWhat gets validated
Object propertiesEach property is validated, then its children are recursed into.
Array itemsEach item in the list is validated against the array's Indices schema. Nested objects inside items are recursed.
Dictionary valuesEach value entry is validated against the dictionary's value schema. Nested objects are recursed.

Built-in rules

  • RequiredRule — fails if value is null, empty string, empty list (IList), or empty dictionary.

Creating a custom validation rule

public class MinLengthRule : ValidationRule
{
public int MinLength { get; set; }

public MinLengthRule(int minLength) => MinLength = minLength;

public override bool IsValid(SchemeModel map, object? value)
{
return value is string s && s.Length >= MinLength;
}

public override string FormatErrorMessage(string name)
{
return $"{name} must be at least {MinLength} characters.";
}
}

Usage:

[RequiredRule, MinLengthRule(3)]
public string Username { get; set; }

OnDataChanged — live edit events

The OnDataChanged callback fires whenever any field in the form is edited. It is debounced by 300ms to avoid excessive calls during rapid typing.

Usage

<DynamicForm Scheme="@_scheme" Data="_formData"
OnValidSubmit="@OnSubmit"
OnDataChanged="@OnEdited">
<SubmitTemplate>
<button type="submit" class="btn btn-primary">Save</button>
</SubmitTemplate>
</DynamicForm>

@code {
private async Task OnEdited(IDictionary<string, object> data)
{
// Auto-save to server
await Http.PostAsJsonAsync("/api/config/draft", data);
}

private async Task OnSubmit(IDictionary<string, object>? data)
{
// Final save
await Http.PostAsJsonAsync("/api/config", data);
}
}

Behavior

  • Debounced — waits 300ms after the last change before firing; rapid edits are coalesced.
  • Full data — the callback receives the complete Data dictionary, not just the changed field.
  • No validationOnDataChanged fires regardless of validation state; use it for drafts/auto-save. Use OnValidSubmit for validated final saves.
  • Optional — if OnDataChanged is not bound, no timer is created (zero overhead).

Data Provider — remote options

The DataProviderAttribute renders a dropdown whose options are fetched from an HTTP endpoint at runtime. The component uses HttpClient to GET the specified path and parses the JSON response as a list of key-value objects.

Attribute properties

PropertyTypeDefaultDescription
PathstringThe HTTP path to fetch (relative to HttpClient.BaseAddress or absolute URL).
ValueFieldstring"value"The JSON field name to use as the option value.
LabelFieldstring"label"The JSON field name to use as the option display text.

Usage

[DataProvider("/api/countries")]
public string Country { get; set; }

[DataProvider("/api/languages", "code", "name")]
public string Language { get; set; }

Expected API response

The endpoint must return a JSON array of objects. Each object should contain at least the fields specified by ValueField and LabelField:

[
{ "value": "it", "label": "Italian" },
{ "value": "en", "label": "English" },
{ "value": "es", "label": "Spanish" }
]

Or with custom field names ([DataProvider("/api/languages", "code", "name")]):

[
{ "code": "it", "name": "Italian" },
{ "code": "en", "name": "English" }
]

Component behavior

  • Loading — shows a disabled "Loading..." dropdown while the request is in-flight.
  • Error — if the request fails, shows the error message in a red-bordered dropdown.
  • Caching — the component caches the result per Path and skips redundant requests on re-render.
  • Field matching — field name lookup is case-insensitive.

Requirements

An HttpClient must be registered in DI (standard in Blazor WebAssembly):

builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

Environment picker — cascading project → env dropdown

The EnvironmentPickerAttribute renders two cascading dropdowns: the user first picks a project, then picks one of that project's environments. The selected environment's id is bound to the decorated property.

This is the right widget when a task needs the user to choose a workflow target in another environment — e.g. ScheduleReminderTask schedules a NEW execution and needs to know which env's workflow to fire. A single-dropdown [DataProvider] listing every env in the workspace would work, but a workspace with many projects and many envs per project becomes a wall of options. The two-step picker scopes the second list to a single project.

Attribute properties

No constructor parameters — the attribute is a marker. The endpoints (paths, response shape, workspace scoping) are fixed in the component so every consumer behaves identically. If you need different endpoints, use [DataProvider] instead.

Usage

[Label("Target workflow")]
[Tooltip("Pick the workflow this task should fire. Must be in the same workspace as the calling execution.")]
[EnvironmentPicker]
public Guid? TargetEnvironmentId { get; set; }

The decorated property must be Guid? — the picker writes the env id or null when the user clears the project selection.

Backing endpoints

The component issues two GETs against HttpClient.BaseAddress:

PathReturns
/api/scheduler/projectsProjects in the current workspace as [{ value: <guid>, label: <name> }, …]
/api/scheduler/projects/{projectId}/environmentsEnvironments under that project, same shape

Both endpoints must be workspace-scoped via the X-Workspace-Id header — the picker has no client-side firewall against listing other workspaces' projects.

Expected response

Both endpoints return a JSON array of objects with value (Guid) and label (string):

[
{ "value": "8c1d6a5e-…", "label": "Customer Onboarding" },
{ "value": "f3e2b1a0-…", "label": "Billing Cron" }
]

Component behavior

  • Initial load — projects dropdown fetches /api/scheduler/projects and shows "Loading projects…" until it resolves.
  • Project selected — environments dropdown becomes enabled, fetches /api/scheduler/projects/{id}/environments, shows "Loading environments…" until it resolves.
  • Project cleared — bound value resets to empty (an env from a different project would be a stale ref).
  • Editing a saved row — the bound property comes back with a Guid but the project picker starts blank. The user has to reselect a project to see the matching env. Trade-off: minor UX papercut on edit, vs. an extra round-trip on every load to resolve the env's parent project.
  • Timeouts / errors — each fetch has a 10-second timeout; failures show an inline error in the relevant dropdown.

Trade-offs vs. [DataProvider]

[EnvironmentPicker][DataProvider("/api/envs")]
Number of dropdowns2 (project → env)1
Endpoints required21
Lookup performance with N projects × M envsPer-project fetch (~M items)Single fetch (~N×M items)
Re-edit UXUser reselects projectPicker remembers env directly
Cross-server reusabilityHard-coded pathsConfigurable per attribute

Use [EnvironmentPicker] when the workspace has many envs and grouping by project helps the user navigate. Use [DataProvider] when the list is small enough to fit in one flat dropdown or when the data isn't env-shaped.


Tooltip attribute

The TooltipAttribute adds a native browser tooltip (hover text) to a field's label. This is the simplest way to provide a short hint to the user.

Usage

[Tooltip("Maximum retry attempts before the task gives up")]
public int MaxRetries { get; set; }

[Tooltip("Comma-separated list of email addresses")]
public string Recipients { get; set; }

The tooltip text appears when the user hovers over the field label. It uses the HTML title attribute — no JavaScript or extra components required.


Documentation attribute — inline documentation

The DocumentationAttribute attaches a documentation panel to any form field. A small book icon appears next to the field's label. Clicking it opens a floating card with a description, an optional code example, and an optional link to the full documentation page.

Attribute properties

PropertyTypeDefaultDescription
DescriptionstringPlain-text explanation of the field's purpose.
DocumentationUrlstringURL to the full documentation page (opens in new tab).
ExamplestringCode or value example displayed in a <pre> block.
ExampleLanguagestring"text"Hint for the example language (for future syntax highlighting).

Usage

[Documentation("The variable name to evaluate in each switch branch.")]
public string Variable { get; set; }

[Documentation("Your API key from the settings dashboard.",
"https://docs.example.com/api-key")]
public string ApiKey { get; set; }

[Documentation("JavaScript expression evaluated at runtime.",
"https://docs.example.com/js-task",
"return context.variables['count'] + 1;")]
public string JSCode { get; set; }

Combining with Tooltip

Both attributes can be used together — the tooltip provides a quick hover hint while the documentation panel provides full context:

[Tooltip("Enter your JS code here"),
Documentation("JavaScript expression evaluated at runtime.",
"https://docs.example.com/js-task",
"return context.variables['count'] + 1;")]
public string JSCode { get; set; }

Panel content

The documentation panel renders in this order:

  1. Description — paragraph of explanatory text.
  2. Example — code block with the example snippet (only if Example is set).
  3. Open full documentation — button linking to DocumentationUrl (only if URL is set).

Behavior

  • The book icon renders inline next to the label, for both Top and Inline label positions.
  • Clicking the icon toggles the documentation panel open/closed.
  • The panel floats above the form content (absolute positioned, z-index: 1050).
  • A close button in the panel header dismisses it.
  • When LabelPosition is None, the icon is not shown (no label to attach to).

Full annotated example

Here is a complete example showing how annotations map to UI:

public class SwitchTask
{
[Field("Variable to evaluate", Required = true)]
[Template(ExecutionVariables.All)]
public string Variable { get; set; }

[Field("Default branch", Grid = 6, Placeholder = "ok")]
[DefaultValueForm("ok")]
public string DefaultBranch { get; set; } = "ok";

[Field("Cases")]
public List<SwitchCase> Cases { get; set; }
}

public class SwitchCase
{
[MultipleSelect(
new[] { "Equals", "Contains", "Regex", ">", "<" },
new[] { "=", "contains", "matches", ">", "<" })]
[Grid(3), Label("Op"), DefaultValueForm("Equals")]
public string Op { get; set; } = "Equals";

[Field("Value", Grid = 5, Required = true)]
[Template(ExecutionVariables.All)]
public string Value { get; set; }

[Field("Branch", Grid = 4, Required = true)]
public string Branch { get; set; }
}

This produces:

  • Variable to evaluate — a full-width template-aware input that autocompletes against workflow variables declared elsewhere in the diagram.
  • Default branch — a half-width string input pre-filled with ok.
  • Cases — a dynamic list rendered as a compact 3-column table (Op / Value / Branch), with a single + Add case affordance. Each row is one SwitchCase:
    • Op — multi-select dropdown (3-column).
    • Value — template-aware input (5-column).
    • Branch — string input (4-column).

Another example with diverse field types

public class Test
{
[MultipleSelect("Italian", "Mandarin", "Ananas")]
public string TTS { get; set; }

[CodeEditor("csharp")]
public string Name { get; set; }

[TextArea]
public string Message { get; set; }

[Placeholder("Enter a value here")]
public string Message2 { get; set; }

public float Limit { get; set; }

[Range(0, 100)]
public int LimitInt { get; set; }

[CodeEditor("javascript")]
public string JSCode { get; set; }

public decimal DecimalLimit { get; set; }

public Colors Colors { get; set; }
}

public enum Colors { Black, Red, Yellow }
PropertyRendered as
TTSMulti-select dropdown (Italian, Mandarin, Ananas)
NameMonaco editor with C# syntax
MessageMulti-line textarea
Message2String input with placeholder
LimitFloat numeric input
LimitIntInteger numeric input (range 0–100)
JSCodeMonaco editor with JavaScript syntax
DecimalLimitDecimal numeric input
ColorsDropdown (Black, Red, Yellow)

Extending the system

Adding a custom renderer for a PropertyType

Override the default component for any built-in type:

config.AddCustomRenderer<MyCustomStringComponent>(PropertyType.String);

Adding a custom attribute-driven renderer

  1. Create an attribute that extends DynamicRendererComponent:
public class RichTextAttribute : DynamicRendererComponent
{
public string Toolbar { get; set; } = "full";
}
  1. Create a Razor component that extends FormComponentBase:
@inherits BlazorDynamicForm.Core.FormComponentBase

<!-- your rich text editor markup -->

@code {
// Access SchemeProperty.Attributes to read RichTextAttribute properties
}
  1. Register the mapping:
config.AddCustomAttributeRenderer<RichTextAttribute, RichTextComponent>();
  1. Use it on a property:
[RichText(Toolbar = "minimal")]
public string Description { get; set; }

Using YAML-defined schemas (no C# class)

You can define forms entirely in YAML without a backing C# class:

string yaml = LoadFromFile("task-config.yaml");
List<Type> attrTypes = BlazorDynamicForm.Utility.DefaultComponents;
SchemeModel scheme = Scheme.GetSchemeFromYaml(attrTypes, yaml);

// Use the scheme directly with <DynamicForm>

This is how the platform renders Task configuration forms in the Process Editor — each Module's Task schema is stored as YAML and loaded at runtime.


Component hierarchy at runtime

When <DynamicForm> renders, it produces a component tree like this:

DynamicForm
└─ ObjectComponent (root object)
├─ LabelComponent
│ └─ StringComponent ← string property
├─ LabelComponent
│ └─ IntComponent ← int property
├─ BoxComponent (collapsible)
│ └─ ObjectComponent ← nested object
│ ├─ StringComponent
│ └─ BooleanComponent
└─ ListComponent ← List<T> property
├─ ObjectComponent ← item 0
│ └─ ...
└─ ObjectComponent ← item 1
└─ ...

Each component inherits from FormComponentBase and receives:

ParameterDescription
SchemeModelThe root schema (for resolving $ref pointers).
SchemePropertyThe property definition for this field.
PropertyNameDisplay name of the property.
Value / ValueChangedTwo-way binding for the field value.
IsFirstWhether this is the root-level component.

Project structure

BlazorDynamicForm/
├── BlazorDynamicForm/ # Blazor component library
│ ├── Attributes/ # Custom attributes (Grid, Label, Box, CodeEditor, etc.)
│ ├── AttributesComponents/ # Wrapper components (BoxComponent, LabelComponent)
│ ├── Components/ # Field renderers (StringComponent, ObjectComponent, etc.)
│ ├── Core/
│ │ ├── DynamicFormConfiguration.cs # Renderer registry
│ │ └── FormComponentBase.cs # Base class for all field components
│ └── Utility.cs # DI registration (AddBlazorDynamicForm)
├── TypeAnnotationParser/ # Schema generation library
│ ├── TypeAnnotationParser.cs # Main parser (reflection-based)
│ ├── SchemeModel.cs # Root schema with References
│ ├── SchemeProperty.cs # Property node + AttributeScheme base
│ ├── PropertyType.cs # Type enum
│ ├── ParserConfiguration.cs # Parser config
│ ├── Generator/
│ │ ├── ObjectGenerator.cs # Data creation/validation from schema
│ │ └── ObjectGeneratorOptions.cs
│ └── Serialization/
│ └── Scheme.cs # YAML serialize/deserialize
├── BlazorDynamicFormTest/ # Test Blazor WASM app
└── TypeAnnotationParser.Test/ # Unit tests