Snk.Net.Forms
10.4.0
dotnet add package Snk.Net.Forms --version 10.4.0
NuGet\Install-Package Snk.Net.Forms -Version 10.4.0
<PackageReference Include="Snk.Net.Forms" Version="10.4.0" />
<PackageVersion Include="Snk.Net.Forms" Version="10.4.0" />
<PackageReference Include="Snk.Net.Forms" />
paket add Snk.Net.Forms --version 10.4.0
#r "nuget: Snk.Net.Forms, 10.4.0"
#:package Snk.Net.Forms@10.4.0
#addin nuget:?package=Snk.Net.Forms&version=10.4.0
#tool nuget:?package=Snk.Net.Forms&version=10.4.0
Snk.Net.Forms
A UI-free form engine. A form is versioned JSON data, not code: a designer produces it, and any renderer (Blazor on the web, native MAUI on a phone) shows it. This package holds only the parts every renderer must agree on:
- the schema (
FormDefinition- sections, fields, rules), - the answers (
FormAnswers- field id → JSON value), - the rule engine (
FormEvaluator- what is visible / required right now), - validation of answers (
FormValidator) and of definitions (DefinitionValidator), - an extensible field type registry (
FieldTypeRegistry), - designer operations (
FormDesigner- add / move / remove / duplicate / rename, pure data), - JSON helpers (
FormJson).
It depends on nothing but the .NET base library (System.Text.Json).
Requirements
- .NET 10.0 or later
Define a form
{
"schemaVersion": 1,
"id": "runway-inspection",
"version": 3,
"title": "Runway inspection",
"sections": [
{
"id": "general",
"title": "General",
"fields": [
{ "id": "inspector", "type": "text", "label": "Inspector", "required": true, "width": "half" },
{ "id": "started", "type": "datetime", "label": "Started", "width": "half" },
{ "id": "condition", "type": "radio", "label": "Surface condition", "required": true,
"options": [ { "value": "good", "label": "Good" }, { "value": "poor", "label": "Poor" } ] },
{ "id": "defects", "type": "table", "label": "Defects", "settings": { "maxRows": 20 },
"columns": [
{ "id": "location", "type": "text", "label": "Location", "required": true },
{ "id": "photos", "type": "attachments", "label": "Photos",
"validation": { "maxFiles": 3, "fileTypes": [ "image/*" ], "maxSizeMb": 5 } }
] },
{ "id": "sig", "type": "signature", "label": "Signature", "required": true }
]
}
],
"rules": [
{ "id": "poor-needs-defects",
"when": { "match": "all", "conditions": [ { "field": "condition", "operator": "equals", "value": "poor" } ] },
"then": [ { "action": "show", "targets": [ "defects" ] }, { "action": "require", "targets": [ "defects" ] } ] }
]
}
(This exact JSON is parsed and exercised by FormJsonTests.)
Fields
| Property | Meaning |
|---|---|
id |
Stable answer key. Field and section ids share one namespace; letters, digits, _, -, starting with a letter or _. |
type |
A type key (below, or one your app registered). |
label, help, placeholder |
Text for the renderer. |
required, readOnly |
required is the starting value; rules can change it. |
width |
full (default), half, third, quarter - web lays out columns, a phone may stack. |
defaultValue |
Initial answer, same shape as the answer. |
options |
[{ "value", "label" }] for choice types. value is what is stored. |
validation |
minLength, maxLength, min, max, pattern, patternMessage, minSelected, maxSelected, fileTypes, maxFiles, maxSizeMb. |
settings |
Free-form JSON object for type-specific extras, e.g. multiple, or layout (vertical / horizontal) for a field that lists its options. |
columns |
Child fields of a table (one row). |
Built-in types and answer shapes
| Type | Answer |
|---|---|
text, textarea, email, phone |
string (pattern is matched against the whole value) |
number |
number (a numeric string is accepted) |
date / time / datetime |
"yyyy-MM-dd" / "HH:mm" or "HH:mm:ss" / ISO 8601 ("2026-09-19T08:30:00Z"; no offset = UTC) |
radio, dropdown |
the option value; dropdown with settings.multiple: true takes an array |
checklist |
array of option values |
checkbox |
bool - "required" means it must be ticked |
attachments |
[{ "id", "name", "contentType", "size", "url" }] (FileRef) |
signature |
{ "imageRef" or "data", "signedBy", "signedAt" } (SignatureValue) |
passfail |
{ "Result": "pass" \| "fail" \| "na", "Comment", "Photos": [FileRef...] } - see below |
table |
array of row objects keyed by column id; settings.minRows / maxRows |
heading, paragraph, divider |
display only - no answer |
A choice value may also be an object with a value property, e.g. { "value": "yes", "at": "2026-09-19T08:30Z" },
for a choice that records when it was made. A fixed checklist grid is a table whose defaultValue pre-fills the
rows and whose label column is readOnly (or a paragraph).
passfail is an inspection item: Pass / Fail / N.A., and on a fail a comment and photos. Unlike the older value
objects its keys are PascalCase (Result, Comment, Photos, constants in PassFailValue); each photo is a plain
FileRef and keeps FileRef's camelCase keys. Keys are read in any case.
{ "Result": "fail", "Comment": "Hose cracked", "Photos": [ { "id": "f1", "name": "hose.jpg", "url": "https://..." } ] }
- Empty until it has a
Result; aResultthat is notpass/fail/naisinvalid. settings.CommentOnFail(bool): a fail needs a comment (commentRequired).settings.PhotoOnFail(bool): a fail needs at least one photo (photoRequired).settings.NoNa(bool): N.A. is not offered and isinvalid. Settings are written PascalCase (FieldSettings.CommentOnFail...) and read in any case.- The comment is at most 1000 characters (
maxLength), on any result. - Rules compare the
Result:{ "field": "item", "operator": "equals", "value": "fail" }(alsonotEquals,in,notIn). - The renderers add photos through the same attachment request as an
attachmentsfield (the request'sFieldis the passfail field), and drop the comment and photos when the answer changes to pass or N.A. - Blazor
SnkFormView, for apps that keep PRIVATE files:UploadApiopens the form's own upload box when there is noOnAttachmentRequestedhandler (the uploaded storage key becomes theFileRefid), andFileLinks(ISnkFileLinkProvider) shows a file stored with its key only through a short-lived signed link. Store keys, never the signed links - they expire. Both are off unless set. PassFailFieldTypehas the readers every renderer uses:ResultOf,CommentOf,PhotosOf,Setting,With, andAnswerText("Pass", "Fail - comment", "N.A.").
Rules
when is a condition group: match is all or any, with conditions and nested groups. Operators:
equals, notEquals, in, notIn (value is an array), isEmpty, isNotEmpty, greaterThan, lessThan
(numbers, dates, times) and contains (text, case-insensitive; or membership in a multi answer).
then lists actions - show, hide, require, optional - each with targets (field or section ids).
- A field/section targeted by any
showaction starts hidden and appears only while such a rule matches. - Matched rules apply in list order; when two disagree about a target, the later one wins.
- A hidden field's answer counts as empty in conditions, so chains collapse correctly.
- Evaluation is iterated to a fixed point with a hard bound, so contradictory rules cannot loop;
FormState.Convergedis false if they never settle, andDefinitionValidatorwarns about the usual cause.
Evaluate and validate
using Snk.Net.Forms;
var form = FormJson.DeserializeDefinition(json);
var registry = FieldTypeRegistry.CreateDefault(); // once, at startup
var answers = FormAnswers.CreateFor(form, registry) // defaults filled in
.Set("inspector", "Sam")
.Set("condition", "poor");
FormState state = FormEvaluator.Evaluate(form, answers); // re-run after each change
bool showDefects = state.IsVisible("defects"); // true
bool mustFill = state.IsRequired("defects"); // true
ValidationResult result = FormValidator.Validate(form, answers, registry, state);
foreach (var e in result.Errors)
Console.WriteLine($"{e.Path}: {e.Message} ({e.Code})"); // e.g. "defects: Defects is required. (required)"
string stored = FormJson.Serialize(answers);
Hidden fields (and fields in hidden sections) are never validated. Table-cell errors carry a path such as
defects[0].location. Code is stable (FieldErrorCodes); Message is an English default a renderer may
localize.
Register a custom type
Apps add their own types without touching the library - a renderer maps the same key to its control.
public sealed class ShiftFieldType() : FieldTypeBase("shift")
{
public override IEnumerable<FieldError> Validate(FieldDefinition field, JsonNode? value, FieldValidationContext context)
{
if (FormValues.AsString(value) is not ("DAY" or "NIGHT"))
yield return context.Error(FieldErrorCodes.Invalid, "Choose a shift.");
}
}
var registry = FieldTypeRegistry.CreateDefault()
.Register(new ShiftFieldType())
// a picker whose options come from app data, not the definition:
.Register(new ChoiceFieldType("users", multiple: true, optionsFromDefinition: false));
A type nobody registered is kept, its answer is kept, only the generic required check applies, and
DefinitionValidator reports it as a warning - it is never dropped silently.
Designer operations
var form = new FormDefinition { Id = "incident", Title = "Incident" };
var section = FormDesigner.AddSection(form, "Details"); // id "details"
var what = FormDesigner.AddField(form, section.Id, FieldTypes.Dropdown, "What happened"); // id "what_happened"
FormDesigner.DuplicateField(form, what.Id); // "what_happened_2"
FormDesigner.RenameId(form, what.Id, "kind"); // rules follow the rename
FormDesigner.RemoveField(form, "what_happened_2"); // rules cleaned up
IReadOnlyList<DefinitionIssue> issues = DefinitionValidator.Validate(form, registry);
bool publishable = !issues.Any(i => i.Severity == IssueSeverity.Error); // "What happened" has no options yet
Removing a field drops it from rule targets, deletes rules whose conditions test it (dropping only the condition would silently widen the rule) and deletes rules left with no targets.
JSON
FormJson writes camelCase, omits nulls, defaults and empty lists, and on read ignores unknown properties,
comments and trailing commas. Answer keys are field ids and are never re-cased. Use FormJson.Options if you
serialize these types yourself.
Not in this version
No expression language, calculated values or "set value" actions; rules cannot target table columns; date
fields have no min/max. schemaVersion above 1 is read but warned about.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Snk.Net.Forms:
| Package | Downloads |
|---|---|
|
Snk.Net.Blazor
SNK .NET Blazor Library |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.4.0 | 108 | 10/24/2026 |