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.
- .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
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
bool
string
- Object:
class
orstruct
(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.
The JSON schema can be easily created in the backend via:
var schema = JsonSchema.FromType<RocketData>();
@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(...);
}
}
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
...
}
}
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 RespectNullableAnnotations
property of the JsonSerializerOptions
which ensures that for instance a non-nullable string (string
) is not being populated with a null
value. 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 Required
attribute next to RegularExpression
attribute because otherwise empty strings are always valid.
You can define custom attrbutes which will change the generated JSON schema as described below.
Add a helper text to inputs:
using NJsonSchema.Annotations;
[AttributeUsage(AttributeTargets.Property)]
class HelperTextAttribute : Attribute, IJsonSchemaExtensionDataAttribute
{
public HelperTextAttribute(string text)
{
ExtensionData = new Dictionary<string, object>()
{
["x-helperText"] = text
};
}
public IReadOnlyDictionary<string, object> ExtensionData { get; }
}
internal record MyConfigurationType(
[property: HelperText("Example: /path/to/mission/data")],
string MissionDataPath,
);
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
}
- When using
[RegularExpression]
attribute on a string property,null
values are not supported anymore. This is because the libraryNJsonSchema
which is used to generate the schema is treating a[RegularExpression]
annotated property as non-nullable and so the schema does not carry nullability information anymore.