Feature Specification: Validation

Created: 2026-09-22 Status: Implemented Input: Reconstructed from the code at d225ebe (src/index.ts: validateConfig, synthesizeNullableLeafErrors, the validate and attemptSubmit handle methods; src/diff.ts: prefillRequiredContainers, schemaAtInstancePath; the unit and DOM tests), docs/usage.md, the pull requests of fediversity/nix-module-form, the history of the former panel/forms-island directory in the fediversity monorepo, and the Claude Code logs of the maintainers.

User Scenarios & Testing (mandatory)

User Story 1 - Check a value before it is applied (Priority: P1)

The embedder has a button that applies the configuration. Before it applies the value, it calls attemptSubmit on the handle. The widget validates the value against the full schema. When the value is not valid, the widget returns the paths of the failing fields and shows each error next to its control. The embedder does not apply the value.

Why this priority: a value that is not valid fails later, in the evaluation of the module, where the error is harder to read.

Independent Test: the unit tests call validateConfig with values that violate required, type, enum, pattern and bounds, and check the returned paths.

Acceptance Scenarios:

  1. Given a value where a required option is absent, When the embedder calls attemptSubmit, Then the result contains the path of that option and the control shows an error.
  2. Given a valid value, When the embedder calls attemptSubmit, Then the result is empty and the form shows no error.
  3. Given a value with a string where the schema needs an integer, When the embedder calls validate, Then the result contains { path, message } for that field.

User Story 2 - No errors on a fresh form (Priority: P1)

A user opens a new form. Many required options are still empty. The form does not show errors until the first validation that fails.

Why this priority: a form full of red errors before the user starts is hard to use.

Independent Test: mount a schema with required options and an empty value, and check that no control shows an error. Then call validate and check that the errors show.

Acceptance Scenarios:

  1. Given a fresh form, When it mounts, Then no control shows an error.
  2. Given a failed validation, When the user edits the form, Then the errors update with each edit.

User Story 3 - One error for one cause (Priority: P2)

A required field inside a nullable object or inside the active branch of a tagged union is empty. The form shows one error at that field, not errors on the wrapper, on the union or on the branches that are not active.

Why this priority: noise from the composition keywords hides the field that the user must fix.

Independent Test: the unit tests validate a value with a missing leaf inside a tagged union and inside a nullable object, and check that the result contains only the leaf path.

Acceptance Scenarios:

  1. Given a nullable object that is set and a required leaf in it that is absent, When the form validates, Then the error shows at the leaf and not at the nullable wrapper.
  2. Given a tagged union with an active branch and a required leaf in it that is absent, When the form validates, Then the result contains the leaf path and no error for the other branches.
  3. Given a required container object that is absent, When the form validates, Then the result contains the required leaves inside the container.

Edge Cases

  • A required boolean is false or a required string is "": these are valid values and the widget does not report them as absent.
  • A nullable object is null: the widget reports no error for the leaves inside it.
  • Something outside JSONForms wrote the value (for example a script): the embedder passes that value to validate or attemptSubmit, and the widget validates it and not its own copy.
  • The value contains synthetic old fields (see 004-schema-changes-and-migration): the widget removes them before it validates.

Requirements (mandatory)

Functional Requirements

  • FR-001: The widget MUST validate the value against the full schema with AJV for draft 2020-12, with all errors, and with pattern in the mode that the JSON Schema specification defines.
  • FR-002: validateConfig(schema, value) MUST work without a mounted form and MUST return a list of { path, message }, where path is a dot path of the field. An empty list means valid.
  • FR-003: The handle MUST offer validate(value?) that returns the same list and shows the errors in the form, and attemptSubmit(value?) that returns only the paths.
  • FR-004: The widget MUST NOT show errors before the first failed validation.
  • FR-005: Before it validates, the widget MUST treat an absent required container that is not nullable as an empty object, so that the required leaves inside it are reported.
  • FR-006: The widget MUST remove errors that name no field the user can change: rejections of oneOf, anyOf and if, "must be null" errors of the null branch of a nullable option, and the errors of the branches of a tagged union that are not active. It MUST remove duplicate errors.
  • FR-007: The widget MUST show a required-leaf error inside a nullable option or a tagged-union branch at the control of that leaf (synthesizeNullableLeafErrors).
  • FR-008: When validation fails, the widget MUST open the collapsed sections that contain the failing fields.
  • FR-009: The widget MUST NOT block other actions of the embedder (for example a save of an incomplete draft). Only the embedder decides what to do with the result.

Key Entities

  • Validation error: a pair of a dot path to a field and a message.
  • Validation mode: hidden until the first failed validation, then shown.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: A value that the widget accepts is also accepted by another validator for draft 2020-12 with the same schema.
  • SC-002: For each missing required leaf, the result has exactly one error, at the path of that leaf.
  • SC-003: A fresh form shows no error.

Assumptions

  • The schema describes the rules of the module well enough that a valid value evaluates. Rules that JSON Schema cannot express (for example assertions of the module) are the concern of the embedder.
  • The embedder calls validate or attemptSubmit when it needs a result. The widget does not validate on a timer.

Known Deviations

None.