BlazorJsonForm
Build Blazor forms from JSON Schema
Install / Use
/learn @Apollo3zehn/BlazorJsonFormREADME
BlazorJsonForm
Introduction
Build Blazor forms from JSON Schema using MudBlazor. Inspiration comes from the JSON Forms project.
The main use case for this library is a Single-Page Blazor application (Wasm) that needs to provide a proper UI for configuration data. The corresponding C# types can be defined in the backend (or in plugins loaded by the backend). Using the external library NJsonSchema it is then easy to generate a JSON schema from these configuration types, send the resulting JSON to the frontend and finally use this library to render a nice UI. The backing store is a JsonNode that can be passed back to the backend as a JSON string when the user's configuration is about to be saved. The backend can easily deserialize the data into a strongly typed instance and validate it afterwards.
Additionally to the validation in the backend, the frontend can validate the input data as well. This can be achieved by using MudForm (MudBlazor) or EditContext (Microsoft).
Here is a live example with a predefined configuration type. It has many properties to test all kinds of data. The Nullable mode button switches between a type without nullable properties and one with only nullable properties (to be able to test both variants).
The Validate form button validates the current state of the form in the frontend. And the Validate object button causes the JSON form data to be deseralized and validated using data annotations validator class. This would normally be done in the backend.
Getting started
Requirements
- .NET 8+
- MudBlazor (installation guide)
Ensure these four components are present at the top level (e.g. in MainLayout.razor):
<MudThemeProvider />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
- this library:
dotnet add package BlazorJsonForm --prerelease
Type definition
The following data types are supported:
- Integer:
byte,int,ulong,long - Floating point:
float,double - Enum, underlying type:
byte,sbyte,ushort,short,uint,int,ulong,long DateTime,TimeSpanboolstring- Object:
classorstruct(includingrecord) - Array:
T[],List<T>,IList<T> - Dictionary:
Dictionary<string, T>,IDictionary<string, T>
All listed types can also be nullable (e.g. int? or string?).
The simplest way to define you configuration type is to use C# records. Make sure to add proper XML documentation to each property.
/// <param name="EngineCount">Number of engines</param>
/// <param name="Fuel">Amount of fuel in L</param>
/// <param name="Message">Message from mankind</param>
/// <param name="LaunchCoordinates">Launch coordinates</param>
record RocketData(
int EngineCount,
double Fuel,
string? Message,
int[] LaunchCoordinates
);
[!NOTE] See also Types.cs for a complete example.
JSON Schema
The JSON schema can be easily created in the backend via:
var schema = JsonSchema.FromType<RocketData>();
var schemaAsJson = schema.ToJson();
Blazor
@if (_schema is not null)
{
<JsonForm Schema="_schema" @bind-Data="_data" />
}
@code
{
private JsonSchema _schema;
private JsonNode? _data;
protected override async Task OnInitializedAsync()
{
_schema = await GetJsonSchemaFromBackendAsync(...);
}
}
Frontend Validation
Wrap JsonForm in a MudForm as shown below and validate the form via _form.Validate():
<MudButton
OnClick="ValidateForm">
Validate Form
</MudButton>
<MudForm @ref="_form">
<JsonForm
Schema="_schema"
@bind-Data="_data" />
</MudForm>
@code
{
// ...
private MudForm _form = default!;
private async Task ValidateForm()
{
await _form.Validate();
if (_form.IsValid)
...
else
...
}
}
Desialization & Backend Validation
As shown above, the actual configuration data is stored in the instance variable _data which is of type JsonNode?.
When the frontend validation succeeds, you can serialize the data via var jsonString = JsonSerializer.Serialize(_data) and send it to the backend.
The backend can then deserialize the JSON string into a strongly-typed object and validate it:
var config = JsonSerializer.Deserialize<RocketData>();
[!NOTE] If you already use .NET 9 you should enable the
RespectNullableAnnotationsproperty of theJsonSerializerOptionswhich ensures that for instance a non-nullable string (string) is not being populated with anullvalue. Otherwise an exception is being thrown.
The deserialized object can be further validated by using the .NET built-in Validator class:
var validationResults = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(
config,
new ValidationContext(config),
validationResults,
validateAllProperties: true
);
The validator validates all properties against certain conditions. These are being expressed using data annotation attributes. Currently, the following three data annotation attributes are supported and tested:
[Range(...)]
int Foo { get; set; }
[StringLength(...)]
string Bar { get; set; }
[RegularExpression(...)]
string FooBar { get; set; }
[!NOTE] You should consider adding the
Requiredattribute next toRegularExpressionattribute because otherwise empty strings are always valid.
Extras
You can define a custom attrbute to change the generated JSON schema as described below.
The attribute definition may look like this:
[AttributeUsage(AttributeTargets.Property)]
internal class JsonSchemaExtensionAttribute(params string[] extensionData) : Attribute, IJsonSchemaExtensionDataAttribute
{
public IReadOnlyDictionary<string, object> ExtensionData { get; } = extensionData
.Select((value, index) => new { PairNum = index / 2, value })
.GroupBy(pair => pair.PairNum)
.Select(group => group.Select(g => g.value).ToArray())
.ToDictionary(x => x[0], x => (object)x[1]);
}
Rename field labels
You can now use the newly defined JsonSchemaExtensionAttribute to define the field labels (via x-label) as follows:
record MyConfigurationType(
[property: JsonSchemaExtension("x-label", "EngineCount_label")]
string MissionDataPath,
);
Dictionary: Rename 'key' and 'value'
It works similar for the key and value labels of a dictionary (via x-keyLabel and x-valueLabel):
record MyConfigurationType(
[property: JsonSchemaExtension(
"x-keyLabel", "Vogon",
"x-valueLabel", "English"
)]
Dictionary<string, string> BabelFishDictionary,
);
Helper text
Or you can add a helper text to inputs via x-helperText:
record MyConfigurationType(
[property: JsonSchemaExtension("x-helperText", "Example: /path/to/mission/data")]
string MissionDataPath,
);
Enum display names
Specify custom enum member names to be displayed in the UI:
using NJsonSchema.Annotations;
[AttributeUsage(AttributeTargets.Enum)]
internal class EnumDisplayNamesAttribute : Attribute, IJsonSchemaExtensionDataAttribute
{
public EnumDisplayNamesAttribute(params string[] displayNames)
{
ExtensionData = new Dictionary<string, object>()
{
["x-enumDisplayNames"] = displayNames
};
}
public IReadOnlyDictionary<string, object> ExtensionData { get; }
}
[EnumDisplayNames(
"The Mercury",
"The Venus",
"The Mars",
"The Jupiter",
"The Saturn",
"The Uranus",
"The Neptune"
)]
internal enum MissionTarget
{
Mercury,
Venus,
Mars,
Jupiter,
Saturn,
Uranus,
Neptune
}
Localization
To enable localization, you only need to register an implementation of the following interface:
public interface IJsonFormLocalizer
{
string GetString(string key);
}
Register it in your DI container using your existing localization system:
builder.Services.AddScoped<IJsonFormLocalizer, Localizer>();
[!NOTE] The Localizer implementation can internally use IStringLocalizer, resource files, databases, or any other localization mechanism you already have. It works for
x-label,x-keyLabel,x-valueLabelandx-helperText.
Example
To localize a field label, define an x-label extension on the corresponding property and ensure that your implementation of IJsonFormLocalizer returns a proper value for the requested label key.
record RocketData(
[property: JsonSchemaExtension("x-label", "EngineCount_label")]
int EngineCount,
[property: JsonSchemaExtension("x-label", "Fuel_label")]
double Fuel
);
Known issues
- When using
[RegularExpression]attribute on a string property,nullvalues are not supported anymore. This is because the libraryNJsonSchemawhich is used to generate the schema is treating a[RegularExpression]annotated property as non-nullable and so the schema does

