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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Snk.Net.Forms" Version="10.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Snk.Net.Forms" Version="10.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Snk.Net.Forms" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Snk.Net.Forms --version 10.4.0
                    
#r "nuget: Snk.Net.Forms, 10.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Snk.Net.Forms@10.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Snk.Net.Forms&version=10.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Snk.Net.Forms&version=10.4.0
                    
Install as a Cake Tool

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; a Result that is not pass / fail / na is invalid.
  • 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 is invalid. 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" } (also notEquals, in, notIn).
  • The renderers add photos through the same attachment request as an attachments field (the request's Field is 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: UploadApi opens the form's own upload box when there is no OnAttachmentRequested handler (the uploaded storage key becomes the FileRef id), and FileLinks (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.
  • PassFailFieldType has the readers every renderer uses: ResultOf, CommentOf, PhotosOf, Setting, With, and AnswerText ("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 show action 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.Converged is false if they never settle, and DefinitionValidator warns 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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 112 10/24/2026