<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hamza Hamidi</title>
    <description>The latest articles on DEV Community by Hamza Hamidi (@hamzahamidi).</description>
    <link>https://dev.to/hamzahamidi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F138712%2F05b76b9e-041e-4d35-a6e1-2e6e96307f63.jpeg</url>
      <title>DEV Community: Hamza Hamidi</title>
      <link>https://dev.to/hamzahamidi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hamzahamidi"/>
    <language>en</language>
    <item>
      <title>Add Gherkin linting to a Node.js project</title>
      <dc:creator>Hamza Hamidi</dc:creator>
      <pubDate>Thu, 08 Oct 2026 09:54:39 +0000</pubDate>
      <link>https://dev.to/hamzahamidi/add-gherkin-linting-to-a-nodejs-project-3m8b</link>
      <guid>https://dev.to/hamzahamidi/add-gherkin-linting-to-a-nodejs-project-3m8b</guid>
      <description>&lt;p&gt;Gherkin feature files can parse correctly and still contain duplicate tags, repeated names or unused Scenario Outline variables. &lt;a href="https://github.com/hamzahamidi/gherkin-refine" rel="noopener noreferrer"&gt;Gherkin Refine&lt;/a&gt; checks those issues in a Node.js project, with optional rules for whitespace cleanup. It statically checks &lt;code&gt;.feature&lt;/code&gt; files. It does not execute scenarios or validate Cucumber step definitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and run it locally
&lt;/h2&gt;

&lt;p&gt;The package requires &lt;a href="https://github.com/hamzahamidi/gherkin-refine/blob/main/package.json" rel="noopener noreferrer"&gt;Node.js 22.18 or later&lt;/a&gt;. Install it as a development dependency, then point it at your feature files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; gherkin-refine
npx gherkin-refine features/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://hamidihamza.com/gherkin-refine/rules" rel="noopener noreferrer"&gt;recommended rules&lt;/a&gt; run without a configuration file. They check duplicate tags, duplicate Feature and Scenario names, and unused or undeclared Scenario Outline variables. Style rules stay off until you choose to enable them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on pull requests
&lt;/h2&gt;

&lt;p&gt;Add a workflow that installs the locked dependencies and lints the same feature directory used locally. The &lt;a href="https://hamidihamza.com/gherkin-refine/getting-started" rel="noopener noreferrer"&gt;Getting started guide&lt;/a&gt; includes a complete GitHub Actions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gherkin&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22.18.0&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx gherkin-refine features/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lint error returns &lt;a href="https://hamidihamza.com/gherkin-refine/getting-started" rel="noopener noreferrer"&gt;exit status 1&lt;/a&gt;, so the job fails. If merges should wait for the result, mark the job as a required check in branch protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring over an existing gherkin-lint setup
&lt;/h2&gt;

&lt;p&gt;If the repository already uses gherkin-lint, replace the dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm uninstall gherkin-lint
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; gherkin-refine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gherkin Refine installs a &lt;code&gt;gherkin-lint&lt;/code&gt; command and reads the existing &lt;code&gt;.gherkin-lintrc&lt;/code&gt; and &lt;code&gt;.gherkin-lintignore&lt;/code&gt;, so scripts and CI keep working. It &lt;a href="https://github.com/hamzahamidi/gherkin-refine/releases/tag/v1.3.0" rel="noopener noreferrer"&gt;supports every gherkin-lint 4.2.4 rule&lt;/a&gt;. A few findings differ where gherkin-lint bugs were fixed, and the &lt;a href="https://hamidihamza.com/gherkin-refine/migration" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; lists them.&lt;/p&gt;

&lt;p&gt;To move to a native configuration instead, preview the conversion before writing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx gherkin-refine migrate .gherkin-lintrc &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://hamidihamza.com/gherkin-refine/migration" rel="noopener noreferrer"&gt;migration command&lt;/a&gt; prints the generated configuration and reports rules it cannot map or behavior that changes. Review that output before running the command without &lt;code&gt;--dry-run&lt;/code&gt; to write &lt;code&gt;gherkin-refine.config.json&lt;/code&gt;. Add &lt;code&gt;--strict&lt;/code&gt; to fail instead of writing a configuration that drops a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview safe whitespace fixes
&lt;/h2&gt;

&lt;p&gt;Whitespace rules are opt in. Add the ones that match your team's style to &lt;code&gt;gherkin-refine.config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"recommended"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"no-extra-blank-lines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"no-trailing-whitespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a dry run to see which safe fixes are available without writing the feature file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx gherkin-refine features/checkout.feature &lt;span class="nt"&gt;--fix-dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dry run reports findings and which ones have safe fixes, but does not show an edited diff. Apply fixes in your working tree and inspect the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx gherkin-refine features/checkout.feature &lt;span class="nt"&gt;--fix&lt;/span&gt;
git diff &lt;span class="nt"&gt;--&lt;/span&gt; features/checkout.feature
npx gherkin-refine features/checkout.feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The clip shows a dry run reporting trailing whitespace and repeated blank lines, applies those whitespace fixes, displays the cleaned feature file and checks it again. Findings without a safe automatic fix remain for you to review. Formatting rules such as &lt;code&gt;indentation&lt;/code&gt; and &lt;code&gt;use-and&lt;/code&gt; also have safe fixes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmc66xl2iuuypzafqjto.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmc66xl2iuuypzafqjto.gif" alt="Gherkin Refine previews whitespace findings, applies safe fixes, and checks the feature file again" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For every built-in rule, see the &lt;a href="https://hamidihamza.com/gherkin-refine/rules" rel="noopener noreferrer"&gt;rules reference&lt;/a&gt;. The &lt;a href="https://hamidihamza.com/gherkin-refine/configuration" rel="noopener noreferrer"&gt;configuration guide&lt;/a&gt;, &lt;a href="https://hamidihamza.com/gherkin-refine/plugins" rel="noopener noreferrer"&gt;plugin guide&lt;/a&gt; and &lt;a href="https://github.com/hamzahamidi/gherkin-refine#api" rel="noopener noreferrer"&gt;API example&lt;/a&gt; cover the next steps. Install the &lt;a href="https://www.npmjs.com/package/gherkin-refine" rel="noopener noreferrer"&gt;package from npm&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gherkin</category>
      <category>node</category>
      <category>testing</category>
      <category>cucumber</category>
    </item>
    <item>
      <title>Six Old Processes Led Me to Build AgentDust</title>
      <dc:creator>Hamza Hamidi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:34:42 +0000</pubDate>
      <link>https://dev.to/hamzahamidi/six-old-processes-led-me-to-build-agentdust-5d4j</link>
      <guid>https://dev.to/hamzahamidi/six-old-processes-led-me-to-build-agentdust-5d4j</guid>
      <description>&lt;p&gt;When my Mac started running hot, I looked at processes left around my AI coding sessions. I found six processes, each 7 to 9 days old: two development servers, a mock server, a browser driver, its MCP server, and a stale Node script. Three held ports. All six showed 0% CPU when I checked, so they were not the cause of the heat.&lt;/p&gt;

&lt;p&gt;I had found them by checking the process list and launchctl by hand. I still had no safe rule for deciding what to stop.&lt;/p&gt;

&lt;p&gt;There are related process leak reports for &lt;a href="https://github.com/anthropics/claude-code/issues/1935" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; and &lt;a href="https://github.com/openai/codex/issues/21008" rel="noopener noreferrer"&gt;Codex&lt;/a&gt;. They describe the same class of lifecycle problem. They do not show that my six processes were all caused by those bugs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4tl2qfplx3nk9n6rkk1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4tl2qfplx3nk9n6rkk1.png" alt="Diagram showing a session marker passing to a helper, then AgentDust classifying, requiring approval, rechecking identity, and signaling one process." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A parent PID does not show whether work is abandoned
&lt;/h2&gt;

&lt;p&gt;Killing every process whose parent PID is 1 sounds like a shortcut. In one test, a background Node job had parent PID 1 after 0.3 seconds even though the agent that launched it was still running. Parent PID describes a process relationship at that moment. It does not tell me whether the work is still needed.&lt;/p&gt;

&lt;p&gt;Names and ports leave the same question unanswered. A process named node could be a test server, a mock, or part of an application I am using. A process holding a port is not necessarily active, abandoned, or safe to stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Track ownership while the session runs
&lt;/h2&gt;

&lt;p&gt;I built AgentDust to associate a process with the Claude Code session whose marker it inherited. Hooks record session and shell events. At session start, a hook writes a random marker to Claude Code's environment file so child processes inherit it, and stores a keyed form in the local journal. During a survey, AgentDust hashes an inherited marker and compares it with the recorded key.&lt;/p&gt;

&lt;p&gt;AgentDust records the agent identity so it can check whether the session's agent is still alive. Processes can be owned by a live session, owned by an ended session, suspect, managed, or unknown. Only processes owned by ended sessions and suspects can enter a cleanup plan. The doctor command only reads and reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approval and revalidation
&lt;/h2&gt;

&lt;p&gt;I approve a cleanup by typing a generated four-character code. The approval form does not accept a code supplied through the model's tool call. Before signaling, AgentDust takes a fresh process inventory, classifies each process again, and compares its boot session, PID, start time, user, and executable path with the identity in the plan. If a process no longer qualifies or its identity changed, AgentDust refuses to signal it. Otherwise it sends one SIGTERM to that PID.&lt;/p&gt;

&lt;p&gt;A narrow PID reuse window remains between the last identity check and the signal call, because the operating system signal call takes a numeric PID. Revalidation reduces that risk but cannot remove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;The hook's measured median latency is 2.5 ms per event on a macos-15 runner. One KERN_PROCARGS2 fuzz run processed 27,196,230 inputs in 61 seconds without a crash. Two clean builds produced byte-identical binaries. The &lt;a href="https://github.com/hamzahamidi/agentdust/blob/main/docs/m0/report.md" rel="noopener noreferrer"&gt;M0 report&lt;/a&gt; records these results.&lt;/p&gt;

&lt;p&gt;AgentDust 0.1.0 supports Claude Code on macOS with Apple silicon. Install it with Homebrew, then connect it to Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;hamzahamidi/agentdust/agentdust
agentdust setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cleanup skill is optional. To add it, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add hamzahamidi/agentdust
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;agentdust@agentdust &lt;span class="nt"&gt;--scope&lt;/span&gt; user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a new Claude Code session and run &lt;code&gt;/agentdust:cleanup&lt;/code&gt;. The &lt;a href="https://github.com/hamzahamidi/agentdust" rel="noopener noreferrer"&gt;repository&lt;/a&gt; has the implementation and setup details. The &lt;a href="https://github.com/hamzahamidi/agentdust/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;0.1.0 release&lt;/a&gt; has the release artifacts.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>macos</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Publishing a Chrome extension from GitHub Actions without a stored secret</title>
      <dc:creator>Hamza Hamidi</dc:creator>
      <pubDate>Sat, 26 Sep 2026 02:56:31 +0000</pubDate>
      <link>https://dev.to/hamzahamidi/publishing-a-chrome-extension-from-github-actions-without-a-stored-secret-lbk</link>
      <guid>https://dev.to/hamzahamidi/publishing-a-chrome-extension-from-github-actions-without-a-stored-secret-lbk</guid>
      <description>&lt;p&gt;Most of the publishing actions I looked at require a long-lived OAuth refresh token in repository secrets, and that credential can publish every extension of the publisher. &lt;a href="https://github.com/marketplace/actions/publish-to-chrome-web-store" rel="noopener noreferrer"&gt;Publish to Chrome Web Store&lt;/a&gt;, a GitHub Action at &lt;a href="https://github.com/hamzahamidi/publish-to-chrome-web-store/releases/tag/v1.2.0" rel="noopener noreferrer"&gt;v1.2.0&lt;/a&gt; since 26 September 2026, gets a 30-minute access token for each release through GitHub OIDC and Workload Identity Federation instead. It uses only the Chrome Web Store API v2, checks the store state before writing, and has no runtime dependencies.&lt;/p&gt;

&lt;p&gt;Google's &lt;a href="https://developer.chrome.com/docs/webstore/api/v1" rel="noopener noreferrer"&gt;v1.1 reference&lt;/a&gt; says the old API is deprecated and only supported until 15 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The credential problem
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;chromewebstore&lt;/code&gt; OAuth scope covers every item of a publisher, not one extension, and once the store approves an upload, Chrome's auto-update delivers it to every user. Google Cloud can trust GitHub's OIDC tokens through Workload Identity Federation, and the Chrome Web Store accepts a &lt;a href="https://developer.chrome.com/docs/webstore/service-accounts" rel="noopener noreferrer"&gt;service account linked to the publisher&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The release job exchanges its OIDC token for an access token that lasts 30 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tag push, approved in the chrome-web-store environment
    ↓  GitHub OIDC token (repository, ref and environment)
Google Workload Identity provider
    ↓  checks owner ID, repository ID, tag ref and environment
service account linked to the publisher
    ↓  access token, chromewebstore scope, 30 minutes
publish-to-chrome-web-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chrome-web-store&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chrome-web-store&lt;/span&gt;
    &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;workload_identity_provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.CWS_WIF_PROVIDER }}&lt;/span&gt;
        &lt;span class="na"&gt;service_account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.CWS_SERVICE_ACCOUNT }}&lt;/span&gt;
        &lt;span class="na"&gt;token_format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;access_token&lt;/span&gt;
        &lt;span class="na"&gt;access_token_scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.googleapis.com/auth/chromewebstore&lt;/span&gt;
        &lt;span class="na"&gt;access_token_lifetime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1800s&lt;/span&gt;
        &lt;span class="na"&gt;create_credentials_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;export_environment_variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hamzahamidi/publish-to-chrome-web-store@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;access-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.auth.outputs.access_token }}&lt;/span&gt;
        &lt;span class="na"&gt;publisher-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-publisher-id&lt;/span&gt;
        &lt;span class="na"&gt;item-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-32-letter-extension-id&lt;/span&gt;
        &lt;span class="na"&gt;zip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;extension.zip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the ZIP in a separate job without &lt;code&gt;id-token: write&lt;/code&gt;, so build dependencies never run next to the token.&lt;/p&gt;

&lt;p&gt;The threat model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protected:&lt;/strong&gt; the authority to update the publisher's items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removed:&lt;/strong&gt; the long-lived store credential in GitHub secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still trusted:&lt;/strong&gt; your release workflow, this action, GitHub, Google Cloud and the service account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still broad:&lt;/strong&gt; the service account can act on every item of the publisher, not one extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of this protection comes from the workflow, not the action. The provider decides which repository, ref and environment get a token, and the environment decides who approves the job. On a private repository, GitHub offers required reviewers only with GitHub Enterprise (&lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments" rel="noopener noreferrer"&gt;GitHub's reference&lt;/a&gt;); without one, limit who can push release tags, as the &lt;a href="https://github.com/hamzahamidi/publish-to-chrome-web-store#repositories-without-environments" rel="noopener noreferrer"&gt;README&lt;/a&gt; describes. The action also accepts an access token minted any other way, or a client ID, client secret and refresh token that it exchanges itself. It cannot tell how a credential was issued. The README has the &lt;a href="https://github.com/hamzahamidi/publish-to-chrome-web-store#setting-up-workload-identity-federation" rel="noopener noreferrer"&gt;setup in eight steps&lt;/a&gt;. My Google Cloud project for it has no billing account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not call the API yourself
&lt;/h2&gt;

&lt;p&gt;Uploading a ZIP and calling &lt;code&gt;publish&lt;/code&gt; takes two requests. Doing it safely on every tag takes more, because the v2 API leaves several things to the caller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;publish&lt;/code&gt; takes no version.&lt;/strong&gt; It submits whatever package was uploaded last, and &lt;a href="https://developer.chrome.com/docs/webstore/api/reference/rest/v2/publishers.items/fetchStatus" rel="noopener noreferrer"&gt;&lt;code&gt;fetchStatus&lt;/code&gt;&lt;/a&gt; does not say which package sits in the draft. If two runs upload to the same item, one can submit the other's package. The action reads the status again after submitting and fails if a different version went to review, but only one writer per item prevents it. The README makes that a requirement, and the &lt;code&gt;concurrency&lt;/code&gt; group in the job above keeps two releases from uploading at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The store refuses many uploads, and says so late.&lt;/strong&gt; A version in review blocks new packages, and so does an approved version waiting to be published. An upload whose version is not higher than the published one is rejected. The action reads the status first and stops before uploading, with a message that names the blocking version. A version that is already published or in review is skipped, so re-running a release is safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uploads can finish later.&lt;/strong&gt; The upload call may answer that processing is still in progress. The action polls the status &lt;a href="https://github.com/hamzahamidi/publish-to-chrome-web-store#what-it-does" rel="noopener noreferrer"&gt;every 10 seconds, up to 30 times&lt;/a&gt;, and fails without submitting if processing takes longer. It waits for an earlier run's upload instead of racing it. When the upload completes at once, it refuses to submit if the store read a different version from the package than the one sent.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dry-run: true&lt;/code&gt; reads the ZIP and the store status, reports what the run would do, and stops before uploading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional controls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Partial rollout.&lt;/strong&gt; &lt;code&gt;deploy-percentage&lt;/code&gt; submits a version to a share of users, and a later run raises it. Google &lt;a href="https://developer.chrome.com/docs/webstore/api/reference/rest/v2/publishers.items/setPublishedDeployPercentage" rel="noopener noreferrer"&gt;allows this&lt;/a&gt; only for items with over 10,000 seven-day active users, and the percentage can only go up. Raising without a package needs &lt;code&gt;rollout-only: true&lt;/code&gt;. An empty &lt;code&gt;zip&lt;/code&gt;, such as a mistyped step output, then fails the run instead of turning a release into a rollout change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signed uploads.&lt;/strong&gt; Once an item is opted in to &lt;a href="https://developer.chrome.com/docs/webstore/update#opt-in-to-verified-crx-uploads" rel="noopener noreferrer"&gt;Verified CRX Uploads&lt;/a&gt;, the store accepts only a CRX signed with the developer's RSA key, so a leaked store token alone cannot upload a package the developer did not sign. A companion action, &lt;code&gt;hamzahamidi/publish-to-chrome-web-store/sign@v1&lt;/code&gt;, signs the ZIP and makes no network request. The recommended workflow runs it in a separate job whose environment holds the key and never receives the store token. Keep the key in a secret of an environment limited to release tags: &lt;a href="https://docs.github.com/en/actions/reference/security/secure-use" rel="noopener noreferrer"&gt;GitHub states&lt;/a&gt; that any user with write access to a repository can read all of its repository secrets. For the same ZIP and key, the output is byte-identical to Chrome's &lt;code&gt;--pack-extension&lt;/code&gt;, which CI checks on every change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why trust it with the token
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The source runs as is.&lt;/strong&gt; About &lt;a href="https://github.com/hamzahamidi/publish-to-chrome-web-store/tree/main/src" rel="noopener noreferrer"&gt;900 lines&lt;/a&gt; of TypeScript importing only Node.js built-ins, with no bundle and no &lt;code&gt;dist/&lt;/code&gt;. Node 24 strips the types when it loads the files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two hosts.&lt;/strong&gt; Credentials go only to &lt;code&gt;chromewebstore.googleapis.com&lt;/code&gt;, and to &lt;code&gt;oauth2.googleapis.com&lt;/code&gt; for the refresh token route. Redirects are refused, and the README lists &lt;a href="https://github.com/hamzahamidi/publish-to-chrome-web-store#every-request-the-action-makes" rel="noopener noreferrer"&gt;every request&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean logs.&lt;/strong&gt; Credentials are masked before the first log line, and store responses are escaped so they cannot start a workflow command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checked releases.&lt;/strong&gt; &lt;a href="https://github.com/hamzahamidi/publish-to-chrome-web-store/blob/main/.github/workflows/ci.yml" rel="noopener noreferrer"&gt;CI&lt;/a&gt; runs the tests on Linux, Windows and macOS, and separate jobs run the action from its &lt;code&gt;action.yml&lt;/code&gt; against a mock store. Releases are immutable, and &lt;code&gt;v1&lt;/code&gt; moves to the newest release through a workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;What the action does not do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not publish a staged version or cancel a pending review. Do both in the dashboard.&lt;/li&gt;
&lt;li&gt;It detects a competing upload only after the fact. Keep one writer per item.&lt;/li&gt;
&lt;li&gt;It refuses ZIP64 archives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What Google imposes on any publishing tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Google Cloud project, since Google only issues store tokens to an OAuth client or a service account.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/webstore/service-accounts" rel="noopener noreferrer"&gt;One service account&lt;/a&gt; per publisher, able to manage every item of that publisher.&lt;/li&gt;
&lt;li&gt;No API call to create an item or change its visibility. After a visibility change, publish once by hand.&lt;/li&gt;
&lt;li&gt;Partial rollout only above &lt;a href="https://developer.chrome.com/docs/webstore/api/reference/rest/v2/publishers.items/setPublishedDeployPercentage" rel="noopener noreferrer"&gt;10,000 seven-day active users&lt;/a&gt;, and only upward.&lt;/li&gt;
&lt;li&gt;Replacing a lost signing key through support, which &lt;a href="https://developer.chrome.com/docs/webstore/update#opt-in-to-verified-crx-uploads" rel="noopener noreferrer"&gt;can take up to a week&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/marketplace/actions/publish-to-chrome-web-store" rel="noopener noreferrer"&gt;Marketplace listing&lt;/a&gt; and the &lt;a href="https://github.com/hamzahamidi/publish-to-chrome-web-store#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; have the setup. Start with &lt;code&gt;dry-run: true&lt;/code&gt;. &lt;a href="https://github.com/hamzahamidi/yoke/releases/tag/v0.1.7" rel="noopener noreferrer"&gt;Yoke 0.1.7&lt;/a&gt; was the first release submitted through it, on 26 September 2026.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>chrome</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>AJSF on Angular 22, and the way up from 0.8.0</title>
      <dc:creator>Hamza Hamidi</dc:creator>
      <pubDate>Thu, 24 Sep 2026 21:44:53 +0000</pubDate>
      <link>https://dev.to/hamzahamidi/ajsf-on-angular-22-and-the-way-up-from-080-244j</link>
      <guid>https://dev.to/hamzahamidi/ajsf-on-angular-22-and-the-way-up-from-080-244j</guid>
      <description>&lt;p&gt;AJSF turns a JSON Schema into an Angular form. &lt;code&gt;@ajsf/core&lt;/code&gt; renders plain HTML, and five framework packages render it with a UI library: &lt;code&gt;@ajsf/material&lt;/code&gt;, &lt;code&gt;@ajsf/primeng&lt;/code&gt;, &lt;code&gt;@ajsf/bootstrap3&lt;/code&gt;, &lt;code&gt;@ajsf/bootstrap4&lt;/code&gt; and &lt;code&gt;@ajsf/bootstrap5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the week of 15 to 21 September 2026, &lt;code&gt;@ajsf/core&lt;/code&gt; had &lt;a href="https://api.npmjs.org/downloads/point/2026-09-15:2026-09-21/@ajsf/core" rel="noopener noreferrer"&gt;2,932 npm downloads&lt;/a&gt;, over five days of data, since npm &lt;a href="https://api.npmjs.org/downloads/range/2026-09-15:2026-09-21/@ajsf/core" rel="noopener noreferrer"&gt;reports none&lt;/a&gt; for 15 and 17 September. As npm's &lt;a href="https://www.npmjs.com/package/@ajsf/core?activeTab=versions" rel="noopener noreferrer"&gt;versions tab&lt;/a&gt; showed them on 24 September, 2,289 of those were version &lt;code&gt;0.8.0&lt;/code&gt;, from August 2022, and 216 in total were &lt;code&gt;14.0.0&lt;/code&gt; or later. So this note is mostly for people still on &lt;code&gt;0.8.0&lt;/code&gt;: which version to move to, what changes on the way, and what a new Angular 22 app needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why so many installs are still on &lt;code&gt;0.8.0&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;No AJSF release shipped between &lt;a href="https://www.npmjs.com/package/@ajsf/core/v/0.8.0" rel="noopener noreferrer"&gt;&lt;code&gt;0.8.0&lt;/code&gt;&lt;/a&gt;, published on 27 August 2022, and &lt;a href="https://www.npmjs.com/package/@ajsf/core/v/14.0.0" rel="noopener noreferrer"&gt;&lt;code&gt;14.0.0&lt;/code&gt;&lt;/a&gt;, published on 15 August 2026.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@ajsf/core@0.8.0&lt;/code&gt; declares &lt;code&gt;@angular/core &amp;gt;=14.0.0&lt;/code&gt;, an open range, so npm installs it next to any later Angular without a warning. &lt;code&gt;@ajsf/material@0.8.0&lt;/code&gt; also peers on &lt;code&gt;@angular/flex-layout&lt;/code&gt;, whose &lt;a href="https://www.npmjs.com/package/@angular/flex-layout?activeTab=versions" rel="noopener noreferrer"&gt;matching releases&lt;/a&gt; require Angular 14, so on Angular 15 or later npm stops with &lt;code&gt;ERESOLVE&lt;/code&gt; unless it runs with &lt;code&gt;--legacy-peer-deps&lt;/code&gt;. Neither means the combination was ever built or tested.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;14.0.0&lt;/code&gt;, the Angular peer ranges are bounded, so npm reports a resolution error instead of installing a combination nobody built. AJSF then followed Angular one major at a time, up to &lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v22.0.0" rel="noopener noreferrer"&gt;&lt;code&gt;22.0.0&lt;/code&gt;&lt;/a&gt; on 12 September 2026. The current release is &lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v22.3.1" rel="noopener noreferrer"&gt;&lt;code&gt;22.3.1&lt;/code&gt;&lt;/a&gt;, published on 24 September 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the version that matches your Angular
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;14.0.0&lt;/code&gt; onward, the AJSF major equals the Angular major it targets, as Angular Material does. Minor and patch numbers are not tied to Angular, so a major can have several releases, such as &lt;code&gt;22.0.0&lt;/code&gt;, &lt;code&gt;22.1.0&lt;/code&gt;, &lt;code&gt;22.2.0&lt;/code&gt;, &lt;code&gt;22.3.0&lt;/code&gt; and &lt;code&gt;22.3.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.npmjs.com/package/@ajsf/core?activeTab=versions" rel="noopener noreferrer"&gt;majors that exist&lt;/a&gt; are &lt;code&gt;14&lt;/code&gt;, &lt;code&gt;16&lt;/code&gt;, &lt;code&gt;17&lt;/code&gt;, &lt;code&gt;18&lt;/code&gt;, &lt;code&gt;19&lt;/code&gt;, &lt;code&gt;20&lt;/code&gt;, &lt;code&gt;21&lt;/code&gt; and &lt;code&gt;22&lt;/code&gt;. There is no release for Angular 15: &lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v16.0.0" rel="noopener noreferrer"&gt;&lt;code&gt;16.0.0&lt;/code&gt;&lt;/a&gt; crossed 15 on the way from 14 to 16.&lt;/p&gt;

&lt;p&gt;A plain &lt;code&gt;npm install @ajsf/material&lt;/code&gt; takes the newest major, which fails to resolve on an older Angular, so name the major that matches the Angular you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @ajsf/core@17 @ajsf/material@17   &lt;span class="c"&gt;# for Angular 17&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@ajsf/*@17&lt;/code&gt; installs &lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v17.1.0" rel="noopener noreferrer"&gt;&lt;code&gt;17.1.0&lt;/code&gt;&lt;/a&gt;, which has none of the fixes listed below for 18 and later. Fixes ship only in the newest release, with no backports to earlier majors, &lt;code&gt;0.8.0&lt;/code&gt; or &lt;code&gt;angular6-json-schema-form&lt;/code&gt;, so an app on an older Angular gets them by moving to Angular 22.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;14.0.0&lt;/code&gt; is not a rewrite: it targets the same Angular 14 as &lt;code&gt;0.8.0&lt;/code&gt;, and the version jump is the Angular-aligned numbering starting.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;@ajsf&lt;/code&gt; release since &lt;code&gt;14.0.0&lt;/code&gt; is published from the repository's &lt;a href="https://github.com/hamzahamidi/ajsf/blob/main/.github/workflows/release.yml" rel="noopener noreferrer"&gt;release workflow&lt;/a&gt; with an npm provenance attestation that records the repository and workflow, and &lt;code&gt;npm audit signatures&lt;/code&gt; verifies it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrade from &lt;code&gt;0.8.0&lt;/code&gt;, one major at a time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remove flex-layout.&lt;/strong&gt; &lt;code&gt;@ajsf/material&lt;/code&gt; no longer depends on &lt;code&gt;@angular/flex-layout&lt;/code&gt;, which is deprecated and has &lt;a href="https://www.npmjs.com/package/@angular/flex-layout?activeTab=versions" rel="noopener noreferrer"&gt;no Angular 16 release&lt;/a&gt;. Unless something else uses it, run &lt;code&gt;npm uninstall @angular/flex-layout --legacy-peer-deps&lt;/code&gt; first: while its &lt;code&gt;14.0.0&lt;/code&gt; beta sits next to a newer Angular, the next install stops with &lt;code&gt;ERESOLVE&lt;/code&gt;. The &lt;code&gt;fxFlex&lt;/code&gt;, &lt;code&gt;fxFlexOrder&lt;/code&gt;, &lt;code&gt;fxFlexOffset&lt;/code&gt;, &lt;code&gt;fxFlexAlign&lt;/code&gt;, &lt;code&gt;fxLayoutAlign&lt;/code&gt; and &lt;code&gt;fxLayoutGap&lt;/code&gt; options and the &lt;code&gt;flex&lt;/code&gt; and &lt;code&gt;section&lt;/code&gt; layout types still apply, as native CSS, for values written with a unit such as &lt;code&gt;30%&lt;/code&gt; or &lt;code&gt;10px&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match your Angular.&lt;/strong&gt; Install the &lt;code&gt;@ajsf&lt;/code&gt; major for the Angular you run, for every &lt;code&gt;@ajsf&lt;/code&gt; package you use, as above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move Angular and AJSF up one major together&lt;/strong&gt; with one &lt;code&gt;ng update&lt;/code&gt; that names every &lt;code&gt;@ajsf&lt;/code&gt; package you use, as &lt;a href="https://angular.dev/update-guide" rel="noopener noreferrer"&gt;Angular's update guide&lt;/a&gt; describes, for example &lt;code&gt;ng update @angular/core@18 @angular/cli@18 @angular/material@18 @ajsf/core@18 @ajsf/material@18&lt;/code&gt;. Without the &lt;code&gt;@ajsf&lt;/code&gt; packages, &lt;code&gt;ng update&lt;/code&gt; stops with &lt;code&gt;Incompatible peer dependencies found&lt;/code&gt;, because &lt;code&gt;@ajsf/material@17&lt;/code&gt; peers on &lt;code&gt;@angular/material ^17.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your forms&lt;/strong&gt; against the list below, then repeat step 3 up to Angular 22.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What a form can notice between &lt;code&gt;0.8.0&lt;/code&gt; and &lt;code&gt;22&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;20.0.0&lt;/code&gt;, &lt;code&gt;21.0.0&lt;/code&gt; and &lt;code&gt;22.0.0&lt;/code&gt; releases changed no export, selector or input, while minors inside those majors changed Bootstrap markup. The changes below, from 16 to 22, can affect what a form shows, validates or submits, or what your project needs to build it. Upgrade notes for 19 to 22 are in the &lt;a href="https://github.com/hamzahamidi/ajsf/tree/main/docs/release-notes" rel="noopener noreferrer"&gt;release notes directory&lt;/a&gt;; for 14 to 18, read each release's page in the &lt;a href="https://github.com/hamzahamidi/ajsf/releases" rel="noopener noreferrer"&gt;GitHub releases&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v16.0.0" rel="noopener noreferrer"&gt;&lt;code&gt;16.0.0&lt;/code&gt;&lt;/a&gt;, Material&lt;/strong&gt;: the widgets use Angular Material's MDC components (&lt;a href="https://github.com/hamzahamidi/ajsf/pull/367" rel="noopener noreferrer"&gt;#367&lt;/a&gt;). A field without an &lt;code&gt;appearance&lt;/code&gt; option renders as &lt;code&gt;fill&lt;/code&gt; instead of &lt;code&gt;standard&lt;/code&gt;. CSS written against &lt;code&gt;.mat-form-field-*&lt;/code&gt; classes needs the &lt;code&gt;.mat-mdc-*&lt;/code&gt; names.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v17.1.0" rel="noopener noreferrer"&gt;&lt;code&gt;17.1.0&lt;/code&gt;&lt;/a&gt;, messages&lt;/strong&gt;: validator and locale messages are corrected (&lt;a href="https://github.com/hamzahamidi/ajsf/pull/374" rel="noopener noreferrer"&gt;#374&lt;/a&gt;), for example the message for a decimal &lt;code&gt;multipleOf&lt;/code&gt; step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;18.x, field validators&lt;/strong&gt;: duplicates under &lt;code&gt;uniqueItems&lt;/code&gt; (&lt;a href="https://github.com/hamzahamidi/ajsf/pull/379" rel="noopener noreferrer"&gt;#379&lt;/a&gt;), exclusive bounds (&lt;a href="https://github.com/hamzahamidi/ajsf/pull/418" rel="noopener noreferrer"&gt;#418&lt;/a&gt;) and an emptied array under &lt;code&gt;minItems&lt;/code&gt; (&lt;a href="https://github.com/hamzahamidi/ajsf/pull/396" rel="noopener noreferrer"&gt;#396&lt;/a&gt;) now mark the field, and &lt;code&gt;dependencies&lt;/code&gt; is checked per field (&lt;a href="https://github.com/hamzahamidi/ajsf/pull/376" rel="noopener noreferrer"&gt;#376&lt;/a&gt;). Decimal &lt;code&gt;multipleOf&lt;/code&gt; steps and emoji in length limits stop showing false errors (&lt;a href="https://github.com/hamzahamidi/ajsf/pull/396" rel="noopener noreferrer"&gt;#396&lt;/a&gt;). &lt;code&gt;isValid&lt;/code&gt; and submit come from ajv, which already judged all of these cases correctly, so what changes is the message on the field.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v19.0.0" rel="noopener noreferrer"&gt;&lt;code&gt;19.0.0&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;format&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;0.8.0&lt;/code&gt; already checked &lt;code&gt;format&lt;/code&gt; with ajv 6. From 19.0.0, ajv 8 with ajv-formats is stricter, so &lt;code&gt;2021-02-30&lt;/code&gt; no longer passes &lt;code&gt;format: date&lt;/code&gt; and &lt;code&gt;a@b&lt;/code&gt; no longer passes &lt;code&gt;format: email&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;19.0.0&lt;/code&gt;, drafts&lt;/strong&gt;: the draft is read from &lt;code&gt;$schema&lt;/code&gt;, and a schema without it is read as draft 7, though draft 4 keywords are still converted, so a draft 4 schema without &lt;code&gt;$schema&lt;/code&gt; keeps working. Draft 4 is the oldest draft supported: a schema declaring draft 3 still renders, with its property-level keywords handled as in the next bullet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;19.0.0&lt;/code&gt;, property-level &lt;code&gt;required&lt;/code&gt;&lt;/strong&gt;: a property-level &lt;code&gt;required: true&lt;/code&gt;, &lt;code&gt;optional&lt;/code&gt; or &lt;code&gt;requires&lt;/code&gt; is dropped with a console warning naming the properties, so that field is no longer required. Move each &lt;code&gt;required: true&lt;/code&gt; name into the parent object's &lt;code&gt;required&lt;/code&gt; array, list there every property without &lt;code&gt;optional: true&lt;/code&gt;, and turn &lt;code&gt;requires&lt;/code&gt; into a &lt;code&gt;dependencies&lt;/code&gt; entry. This is the change most likely to need a schema edit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;19.0.0&lt;/code&gt;, references and selects&lt;/strong&gt;: &lt;code&gt;$ref&lt;/code&gt; against a schema's own &lt;code&gt;id&lt;/code&gt; resolves. A select over a numeric enum submits numbers instead of strings, and clearing an optional select with its None option no longer makes the form invalid.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;19.0.0&lt;/code&gt;, counts and buttons&lt;/strong&gt;: &lt;code&gt;minProperties&lt;/code&gt; and &lt;code&gt;maxProperties&lt;/code&gt; count only entered values: undefined, null and the empty string do not count, &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;0&lt;/code&gt; do. A layout-declared submit button honours &lt;code&gt;disableInvalidSubmit&lt;/code&gt;, and fixed tuple positions show no remove button.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;20.0.0&lt;/code&gt; and &lt;code&gt;22.0.0&lt;/code&gt;, toolchain&lt;/strong&gt;: moving to 20 needs Node &lt;code&gt;^20.19.0 || ^22.12.0 || &amp;gt;=24.0.0&lt;/code&gt; (&lt;a href="https://github.com/hamzahamidi/ajsf/blob/main/docs/release-notes/20.md" rel="noopener noreferrer"&gt;20 notes&lt;/a&gt;). At 22, TypeScript must be &lt;code&gt;&amp;gt;=6.0 &amp;lt;6.1&lt;/code&gt; and Node &lt;code&gt;^22.22.3 || ^24.15.0 || &amp;gt;=26.0.0&lt;/code&gt; (&lt;a href="https://github.com/hamzahamidi/ajsf/blob/main/docs/release-notes/22.md" rel="noopener noreferrer"&gt;22 notes&lt;/a&gt;). TypeScript 6 turns &lt;code&gt;strict&lt;/code&gt; on when &lt;code&gt;tsconfig.json&lt;/code&gt; does not set it, so set &lt;code&gt;"strict": false&lt;/code&gt; or fix the new errors, and it deprecates &lt;code&gt;baseUrl&lt;/code&gt; with &lt;code&gt;TS5101&lt;/code&gt;: without &lt;code&gt;baseUrl&lt;/code&gt;, each &lt;code&gt;paths&lt;/code&gt; target needs a leading &lt;code&gt;./&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v20.1.0" rel="noopener noreferrer"&gt;&lt;code&gt;20.1.0&lt;/code&gt;&lt;/a&gt;, Bootstrap 4 classes&lt;/strong&gt;: &lt;code&gt;@ajsf/bootstrap4&lt;/code&gt; emits Bootstrap 4 class names instead of Bootstrap 3 ones, for example &lt;code&gt;is-invalid&lt;/code&gt; instead of &lt;code&gt;has-error&lt;/code&gt;. The &lt;a href="https://github.com/hamzahamidi/ajsf/blob/main/projects/ajsf-bootstrap4/README.md" rel="noopener noreferrer"&gt;package README&lt;/a&gt; tables every replacement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v22.1.0" rel="noopener noreferrer"&gt;&lt;code&gt;22.1.0&lt;/code&gt;&lt;/a&gt;, Bootstrap 4 and 5 checkboxes and radios&lt;/strong&gt;: the input and its label render as siblings instead of the label wrapping the input, except Bootstrap 4 toggle buttons, which stay nested. CSS selecting &lt;code&gt;label &amp;gt; input&lt;/code&gt; no longer matches.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-check"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-check-input"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"control1"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-check-label"&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"control1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Accept&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snippet matches Bootstrap 4 and the Bootstrap 5 radios and checkbox lists. For a single Bootstrap 5 checkbox, the input and label share a &lt;code&gt;div&lt;/code&gt; nested inside &lt;code&gt;.form-check&lt;/code&gt;, so a descendant selector matches every case.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v22.2.0" rel="noopener noreferrer"&gt;&lt;code&gt;22.2.0&lt;/code&gt;&lt;/a&gt;, Bootstrap arrays&lt;/strong&gt;: an array title renders once, on the &lt;code&gt;&amp;lt;legend&amp;gt;&lt;/code&gt;, in all three Bootstrap packages. In Bootstrap 4 and 5, every field sits in a wrapper row that carries &lt;code&gt;is-invalid&lt;/code&gt; and, for an array item, holds the remove button beside the field, so a selector such as &lt;code&gt;.input-group.is-invalid&lt;/code&gt; no longer matches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v22.3.0" rel="noopener noreferrer"&gt;&lt;code&gt;22.3.0&lt;/code&gt;&lt;/a&gt;, Bootstrap messages&lt;/strong&gt;: in all three Bootstrap packages, a field's validation message appears under it once the field is touched or changed (&lt;a href="https://github.com/hamzahamidi/ajsf/pull/524" rel="noopener noreferrer"&gt;#524&lt;/a&gt;). Bootstrap 5 puts &lt;code&gt;is-invalid&lt;/code&gt; on the wrapper around the field, not on the input, and the red border on a touched invalid input comes from AJSF's own &lt;code&gt;.ng-invalid.ng-touched&lt;/code&gt; rule.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;New packages&lt;/strong&gt;: &lt;code&gt;@ajsf/bootstrap5&lt;/code&gt; is released from &lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v18.0.0" rel="noopener noreferrer"&gt;&lt;code&gt;18.0.0&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;@ajsf/primeng&lt;/code&gt; from &lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v19.2.0" rel="noopener noreferrer"&gt;&lt;code&gt;19.2.0&lt;/code&gt;&lt;/a&gt;. Neither existed in &lt;code&gt;0.8.0&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before each release, 74 recorded schemas are rendered in every framework, &lt;a href="https://github.com/hamzahamidi/ajsf/blob/main/docs/release-notes/20.md" rel="noopener noreferrer"&gt;444 cases&lt;/a&gt; in all, and their control count and validity are compared with a recorded baseline. That baseline holds no class names, so the Bootstrap markup changes above do not show in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A new Angular 22 app
&lt;/h2&gt;

&lt;p&gt;I built and ran this setup in Chrome on 24 September 2026 with Angular CLI 22.2.0, Angular Material 22.2.0 and &lt;code&gt;@ajsf&lt;/code&gt; 22.3.1. The CLI scaffolds a standalone, zoneless app, and this Material setup needs neither &lt;code&gt;zone.js&lt;/code&gt; nor &lt;code&gt;@angular/animations&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @angular/cli@22 new ajsf-demo &lt;span class="nt"&gt;--defaults&lt;/span&gt; &lt;span class="nt"&gt;--style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;css
&lt;span class="nb"&gt;cd &lt;/span&gt;ajsf-demo
npx ng add @angular/material
npm &lt;span class="nb"&gt;install&lt;/span&gt; @ajsf/material
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ng add @angular/material&lt;/code&gt; asks whether to proceed and which palette, and its defaults give the setup below: it installs Material and the CDK, writes a theme and links the fonts. &lt;code&gt;@ajsf/material&lt;/code&gt; brings &lt;code&gt;@ajsf/core&lt;/code&gt; with it; an app that already lists &lt;code&gt;@ajsf/core&lt;/code&gt; names it in the install too, so both stay on the same version.&lt;/p&gt;

&lt;p&gt;Nothing goes in &lt;code&gt;app.config.ts&lt;/code&gt;: &lt;code&gt;MaterialDesignFrameworkModule&lt;/code&gt; goes in the component's &lt;code&gt;imports&lt;/code&gt; and provides what the form needs. An app that still bootstraps an &lt;code&gt;NgModule&lt;/code&gt; adds it to that module's &lt;code&gt;imports&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;src/app/app.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JsonPipe&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MaterialDesignFrameworkModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ajsf/material&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;MaterialDesignFrameworkModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JsonPipe&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Age&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Plan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;team&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;submitted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submitted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;src/app/app.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;json-schema-form&lt;/span&gt;
  &lt;span class="na"&gt;[schema]=&lt;/span&gt;&lt;span class="s"&gt;"schema"&lt;/span&gt;
  &lt;span class="na"&gt;framework=&lt;/span&gt;&lt;span class="s"&gt;"material-design"&lt;/span&gt;
  &lt;span class="na"&gt;(onSubmit)=&lt;/span&gt;&lt;span class="s"&gt;"onSubmit($event)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/json-schema-form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;{{ submitted() | json }}&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser showed Name, Age and Plan as Material form fields and a select, with the required asterisk on Name and Submit disabled. Touching Name and leaving it empty showed &lt;code&gt;This field is required.&lt;/code&gt;, and typing 16 in Age and leaving the field showed &lt;code&gt;Must be 18 or more&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With valid values, Submit called the handler with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada Lovelace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Age arrived as a number.&lt;/p&gt;

&lt;p&gt;The production bundle was 1.40 MB, and a new project fails the build above &lt;a href="https://github.com/angular/angular-cli/blob/v22.2.0/packages/schematics/angular/application/index.ts" rel="noopener noreferrer"&gt;1 MB&lt;/a&gt;. Raise the &lt;code&gt;initial&lt;/code&gt; entry under &lt;code&gt;budgets&lt;/code&gt; in the &lt;code&gt;production&lt;/code&gt; configuration of &lt;code&gt;angular.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"initial"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maximumWarning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2MB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maximumError"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.5MB"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build also warns that &lt;code&gt;ajv&lt;/code&gt; and &lt;code&gt;ajv-formats&lt;/code&gt; are CommonJS. Listing them in &lt;code&gt;allowedCommonJsDependencies&lt;/code&gt; in the build &lt;code&gt;options&lt;/code&gt; silences the warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"allowedCommonJsDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ajv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ajv-formats"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example leaves &lt;code&gt;loadExternalAssets&lt;/code&gt; off. If your templates set it, bind it as &lt;code&gt;[loadExternalAssets]="true"&lt;/code&gt;: a project created by &lt;code&gt;ng new&lt;/code&gt; rejects the plain attribute &lt;code&gt;loadExternalAssets="true"&lt;/code&gt; with &lt;code&gt;TS2322&lt;/code&gt;. It loads CDN assets for trying the library out (for Material only the Roboto and Material Icons fonts, not a theme), so a production app loads its own and leaves the input off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching framework
&lt;/h2&gt;

&lt;p&gt;Four things change for Bootstrap 5: the package, the module import, the &lt;code&gt;framework&lt;/code&gt; value and the stylesheet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @ajsf/bootstrap5 bootstrap@5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-import { MaterialDesignFrameworkModule } from '@ajsf/material';
&lt;/span&gt;&lt;span class="gi"&gt;+import { Bootstrap5FrameworkModule } from '@ajsf/bootstrap5';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  imports: [MaterialDesignFrameworkModule, JsonPipe],
&lt;/span&gt;&lt;span class="gi"&gt;+  imports: [Bootstrap5FrameworkModule, JsonPipe],
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  framework="material-design"
&lt;/span&gt;&lt;span class="gi"&gt;+  framework="bootstrap-5"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;angular.json&lt;/code&gt;, replace &lt;code&gt;src/material-theme.scss&lt;/code&gt; in the &lt;code&gt;styles&lt;/code&gt; array with the Bootstrap stylesheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"styles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"node_modules/bootstrap/dist/css/bootstrap.min.css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"src/styles.css"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Bootstrap 5 bundle is 827 kB, which passes the default 1 MB error budget, though the build still warns above &lt;a href="https://github.com/angular/angular-cli/blob/v22.2.0/packages/schematics/angular/application/index.ts" rel="noopener noreferrer"&gt;500 kB&lt;/a&gt;. Bootstrap 4 works the same way with &lt;code&gt;@ajsf/bootstrap4&lt;/code&gt;, &lt;code&gt;bootstrap@4&lt;/code&gt;, &lt;code&gt;Bootstrap4FrameworkModule&lt;/code&gt; and &lt;code&gt;framework="bootstrap-4"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;PrimeNG works the same way with &lt;code&gt;PrimengFrameworkModule&lt;/code&gt; and &lt;code&gt;framework="primeng"&lt;/code&gt;, plus &lt;code&gt;providePrimeNG&lt;/code&gt; with a theme preset in &lt;code&gt;app.config.ts&lt;/code&gt;, as the &lt;a href="https://github.com/hamzahamidi/ajsf/blob/main/projects/ajsf-primeng/README.md" rel="noopener noreferrer"&gt;&lt;code&gt;@ajsf/primeng&lt;/code&gt; README&lt;/a&gt; shows. Its bundle is &lt;a href="https://github.com/hamzahamidi/ajsf/blob/main/projects/ajsf-primeng/README.md" rel="noopener noreferrer"&gt;1.55 MB&lt;/a&gt;, so it needs the raised budget too.&lt;/p&gt;

&lt;p&gt;An app can use several framework packages at once: import each framework module in the component that renders its form, and give that form the matching &lt;code&gt;framework&lt;/code&gt; value. The &lt;a href="https://hamidihamza.com/ajsf/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; also switches between them at runtime. The &lt;code&gt;framework&lt;/code&gt; values are &lt;code&gt;material-design&lt;/code&gt;, &lt;code&gt;primeng&lt;/code&gt;, &lt;code&gt;bootstrap-3&lt;/code&gt;, &lt;code&gt;bootstrap-4&lt;/code&gt;, &lt;code&gt;bootstrap-5&lt;/code&gt; and &lt;code&gt;no-framework&lt;/code&gt;, the default, which &lt;code&gt;JsonSchemaFormModule&lt;/code&gt; from &lt;code&gt;@ajsf/core&lt;/code&gt; renders as plain HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which JSON Schema it understands
&lt;/h2&gt;

&lt;p&gt;AJSF understands draft 4, draft 6 and draft 7. Draft 4 keywords are converted whether or not a schema declares &lt;code&gt;$schema&lt;/code&gt;, so a draft 4 schema without it keeps working.&lt;/p&gt;

&lt;p&gt;A declared draft 4, 6 or 7 &lt;code&gt;$schema&lt;/code&gt; is recognised over &lt;code&gt;http&lt;/code&gt; or &lt;code&gt;https&lt;/code&gt; and with or without the trailing &lt;code&gt;#&lt;/code&gt;, from &lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v22.3.1" rel="noopener noreferrer"&gt;&lt;code&gt;22.3.1&lt;/code&gt;&lt;/a&gt;. 2019-09 and 2020-12 are not supported: a schema declaring either fails to compile, and the form does not render.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;if&lt;/code&gt;, &lt;code&gt;then&lt;/code&gt; and &lt;code&gt;else&lt;/code&gt; validate, so a conditionally required field keeps Submit disabled. The layout is built once, though, so that field shows no required marker and no message, even after it is touched. &lt;code&gt;readOnly&lt;/code&gt; and &lt;code&gt;writeOnly&lt;/code&gt; are accepted and have no effect.&lt;/p&gt;

&lt;p&gt;The playground has &lt;a href="https://hamidihamza.com/ajsf/llms.txt" rel="noopener noreferrer"&gt;71 example schemas&lt;/a&gt;, each viewable with Material, PrimeNG, Bootstrap 3, 4 or 5, or plain HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  What isn't there yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Support for 2019-09 and 2020-12, and a layout that follows &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;then&lt;/code&gt; and &lt;code&gt;else&lt;/code&gt;, both described above.&lt;/li&gt;
&lt;li&gt;A single validator. JSON Schema is validated twice today, by &lt;code&gt;ajv&lt;/code&gt; for &lt;code&gt;isValid&lt;/code&gt; and &lt;code&gt;validationErrors&lt;/code&gt; and by hand-written per-field validators, and the two can disagree.&lt;/li&gt;
&lt;li&gt;A daisyUI package is next on the &lt;a href="https://github.com/hamzahamidi/ajsf/blob/main/docs/roadmap.md" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;. ng-zorro and Ionic wait until users ask for them in an &lt;a href="https://github.com/hamzahamidi/ajsf/issues" rel="noopener noreferrer"&gt;issue&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  History and links
&lt;/h2&gt;

&lt;p&gt;AJSF has continued &lt;a href="https://github.com/dschnelldavis/angular2-json-schema-form" rel="noopener noreferrer"&gt;dschnelldavis/angular2-json-schema-form&lt;/a&gt; in its own repository since &lt;a href="https://github.com/hamzahamidi/ajsf/commit/b148449f07b5b475a993d24fc443d0c7a62555ab" rel="noopener noreferrer"&gt;July 2018&lt;/a&gt;. It first shipped as the single package &lt;code&gt;angular6-json-schema-form&lt;/code&gt;, whose last release is &lt;a href="https://www.npmjs.com/package/angular6-json-schema-form/v/8.0.0" rel="noopener noreferrer"&gt;&lt;code&gt;8.0.0&lt;/code&gt;&lt;/a&gt;, and later moved to the &lt;code&gt;@ajsf&lt;/code&gt; scope. What it led to is in &lt;a href="https://hamidihamza.com/notes/texaryn-json-schema-forms/" rel="noopener noreferrer"&gt;my note on Texaryn&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hamidihamza.com/ajsf/" rel="noopener noreferrer"&gt;Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hamidihamza.com/ajsf/api/" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hamidihamza.com/ajsf/llms.txt" rel="noopener noreferrer"&gt;&lt;code&gt;llms.txt&lt;/code&gt; for AI assistants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hamzahamidi/ajsf#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hamzahamidi/ajsf/releases" rel="noopener noreferrer"&gt;Release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/hamzahamidi/ajsf/issues" rel="noopener noreferrer"&gt;Issues&lt;/a&gt;. Security reports go through GitHub &lt;a href="https://github.com/hamzahamidi/ajsf/security/policy" rel="noopener noreferrer"&gt;private vulnerability reporting&lt;/a&gt;, not a public issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://hamidihamza.com/notes/ajsf-angular-22-upgrade/" rel="noopener noreferrer"&gt;hamidihamza.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>forms</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Texaryn: one JSON Schema, rendered in React, Vue or a web component</title>
      <dc:creator>Hamza Hamidi</dc:creator>
      <pubDate>Thu, 24 Sep 2026 10:06:45 +0000</pubDate>
      <link>https://dev.to/hamzahamidi/texaryn-one-json-schema-rendered-in-react-vue-or-a-web-component-85c</link>
      <guid>https://dev.to/hamzahamidi/texaryn-one-json-schema-rendered-in-react-vue-or-a-web-component-85c</guid>
      <description>&lt;p&gt;Texaryn is a headless runtime for JSON Schema forms. A schema adapter reads Draft 7, 2019-09 or 2020-12, a framework-neutral core owns state, validation and submission, and a React, Vue or Web Components binding renders the result.&lt;/p&gt;

&lt;p&gt;If you've shipped react-jsonschema-form, here's what's different: array row identity lives in the runtime and follows the row's data or an &lt;code&gt;itemKey&lt;/code&gt; you name, each field chooses whether it validates on blur, change or submit, and the same runtime renders under React, Vue or a web component. It's pre-1.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I've maintained &lt;a href="https://github.com/hamzahamidi/ajsf" rel="noopener noreferrer"&gt;AJSF&lt;/a&gt;, the Angular JSON Schema form library, since 2018. It continues &lt;code&gt;dschnelldavis/angular2-json-schema-form&lt;/code&gt; in its own repository: &lt;a href="https://github.com/hamzahamidi/ajsf/commit/b148449f07b5b475a993d24fc443d0c7a62555ab" rel="noopener noreferrer"&gt;the first commit&lt;/a&gt;, in July 2018, brought that code to Angular 6. Its major version follows the Angular major it targets, and &lt;a href="https://github.com/hamzahamidi/ajsf/releases/tag/v22.2.1" rel="noopener noreferrer"&gt;&lt;code&gt;22.2.1&lt;/code&gt;&lt;/a&gt; is on Angular 22.&lt;/p&gt;

&lt;p&gt;It has &lt;a href="https://github.com/hamzahamidi/ajsf" rel="noopener noreferrer"&gt;359 stars&lt;/a&gt; and &lt;a href="https://github.com/hamzahamidi/ajsf/forks" rel="noopener noreferrer"&gt;177 forks&lt;/a&gt;, and &lt;code&gt;@ajsf/core&lt;/code&gt; had &lt;a href="https://api.npmjs.org/downloads/point/2026-08-23:2026-09-21/@ajsf/core" rel="noopener noreferrer"&gt;17,630 npm downloads from 23 August to 21 September 2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;AJSF already keeps the schema logic apart from the design system (Material, Bootstrap, PrimeNG), but all of it runs inside Angular. Texaryn grew out of that, and out of exploring what AI needs to build interfaces: describe the interface as structured data, let a runtime own state and validation, and keep rendering in a thin layer per framework.&lt;/p&gt;

&lt;p&gt;In RJSF, &lt;code&gt;Form&lt;/code&gt; processes the schema, manages state and renders, all in React. I wanted three separate owners: an adapter that understands JSON Schema, a runtime that owns form state, and a binding that renders a framework-neutral description of the form.&lt;/p&gt;

&lt;p&gt;The first user I had in mind builds form-heavy React apps from JSON Schema: configuration screens, multi-step onboarding, admin panels, data entry tools. They want per-field dirty and touched state, validation timing per field, and custom components without forking a theme, in React today without being locked into React. Vue was the second binding on purpose: &lt;code&gt;setup&lt;/code&gt; runs once and reactivity is tracked per ref, where React re-runs a component on every render, so it shows whether the binding contract quietly assumed React's model.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JSON Schema
    ↓  adapter (@texaryn/schema-json)
SchemaProjection
    ↓  compiler (@texaryn/core)
UIDocument
    ↓  runtime (@texaryn/core)
FormRuntime state
    ↓  binding (@texaryn/react, @texaryn/vue, @texaryn/web-components)
Widgets (a default set per binding; Bootstrap 5 and Material UI for React)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createJsonSchemaAdapter&lt;/code&gt; returns an object that implements &lt;code&gt;SchemaEvaluationPort&lt;/code&gt;, the interface the runtime calls to project and validate data. A projection is the fields, constraints and active branches for the current data, so it's recomputed as the user edits.&lt;/p&gt;

&lt;p&gt;The compiler turns the projection into a &lt;code&gt;UIDocument&lt;/code&gt;: a versioned tree of typed nodes that says what the form contains, not how React, Vue or the DOM draws it. A binding receives that document rather than the schema, so conditionals, composition and dependencies are evaluated in one place, the adapter, and no binding reimplements them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FormRuntime&lt;/code&gt; owns values, validation, dirty and touched state, visibility, disabled state, submission and array identity. Bindings subscribe to it instead of keeping their own copy. The core has no dependencies at all, and the UI definition is data: no generated JavaScript and no &lt;code&gt;eval()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the rendering side, a binding (&lt;code&gt;@texaryn/react&lt;/code&gt;, &lt;code&gt;@texaryn/vue&lt;/code&gt;, &lt;code&gt;@texaryn/web-components&lt;/code&gt;) connects the runtime to a framework. A registry maps each UI node to a component, and a widget set such as &lt;code&gt;@texaryn/react-bootstrap&lt;/code&gt; or &lt;code&gt;@texaryn/react-mui&lt;/code&gt; is a ready-made registry for React.&lt;/p&gt;

&lt;p&gt;The repository checks the neutrality claim: every example in its catalog runs through each binding and widget set, and the build fails when a documented feature has no example. The &lt;a href="https://texaryn.github.io/texaryn/playground/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; shows the same examples through React Default, React Bootstrap 5, React Material UI, Vue Default and Web Components Default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema
&lt;/h2&gt;

&lt;p&gt;All three examples render this &lt;code&gt;schema.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://json-schema.org/draft/2020-12/schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Age&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;required&lt;/code&gt; only checks that the key exists, and &lt;code&gt;''&lt;/code&gt; counts as a value, so &lt;code&gt;minLength: 1&lt;/code&gt; is what rejects an empty name.&lt;/p&gt;

&lt;h2&gt;
  
  
  In React
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @texaryn/core @texaryn/schema-json @texaryn/react react react-dom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@texaryn/react&lt;/code&gt; needs &lt;a href="https://www.npmjs.com/package/@texaryn/react" rel="noopener noreferrer"&gt;React 18 or newer&lt;/a&gt;. The packages are ES modules only, and the examples await the adapter at module top level, which needs a build target that supports it, such as &lt;code&gt;es2022&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createJsonSchemaAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@texaryn/schema-json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ErrorSummary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;FormProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;FormRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createDefaultRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@texaryn/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createJsonSchemaAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDefaultRegistry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;hints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ada Lovelace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validationTrigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormProvider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorSummary&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormRoot&lt;/span&gt; &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
        &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;validating&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submitting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Submit
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FormProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;useForm&lt;/code&gt; creates the runtime and subscribes to its stores, and &lt;code&gt;FormProvider&lt;/code&gt; makes it available to &lt;code&gt;ErrorSummary&lt;/code&gt; and &lt;code&gt;FormRoot&lt;/code&gt;. &lt;code&gt;FormRoot&lt;/code&gt; renders fields only, with no &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; around them, so the button dispatches the &lt;code&gt;Submit&lt;/code&gt; command itself and Enter doesn't submit. With an empty name, &lt;code&gt;onSubmit&lt;/code&gt; isn't called, and &lt;code&gt;ErrorSummary&lt;/code&gt; takes focus under the heading "There is a problem" and lists &lt;code&gt;Name: Value `#/name` should have a minimum length of `1`, but got `0`.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That message comes from the validator, and there's no way to rewrite it yet. The default widgets set &lt;code&gt;aria-invalid&lt;/code&gt; and link &lt;code&gt;aria-describedby&lt;/code&gt; through &lt;code&gt;getInputProps&lt;/code&gt; and &lt;code&gt;getErrorProps&lt;/code&gt;, which a custom widget can call too.&lt;/p&gt;

&lt;p&gt;Hints go in the same options as &lt;code&gt;initialData&lt;/code&gt;, keyed by JSON Pointer. A field hint sets &lt;code&gt;widget&lt;/code&gt;, &lt;code&gt;order&lt;/code&gt;, &lt;code&gt;placeholder&lt;/code&gt;, &lt;code&gt;helpText&lt;/code&gt; or &lt;code&gt;validationTrigger&lt;/code&gt; (&lt;code&gt;'blur'&lt;/code&gt;, &lt;code&gt;'change'&lt;/code&gt; debounced 300 ms by default, or &lt;code&gt;'submit'&lt;/code&gt;), and an array hint adds &lt;code&gt;itemKey&lt;/code&gt; and &lt;code&gt;canReorder&lt;/code&gt;. A field without a &lt;code&gt;validationTrigger&lt;/code&gt; validates only on submit, so in the Vue and web component examples below an error stays until the next submit.&lt;/p&gt;

&lt;p&gt;Submission is a state machine: &lt;code&gt;idle&lt;/code&gt;, &lt;code&gt;validating&lt;/code&gt;, &lt;code&gt;submitting&lt;/code&gt;, &lt;code&gt;submitted&lt;/code&gt;. Submit validates an immutable snapshot of the data, and an edit during &lt;code&gt;validating&lt;/code&gt; cancels the attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Vue
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @texaryn/core @texaryn/schema-json @texaryn/vue vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@texaryn/vue&lt;/code&gt; needs &lt;a href="https://www.npmjs.com/package/@texaryn/vue" rel="noopener noreferrer"&gt;Vue 3.5 or newer&lt;/a&gt;. &lt;code&gt;main.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createJsonSchemaAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@texaryn/schema-json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./App.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createJsonSchemaAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;App.vue&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SchemaEvaluationPort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@texaryn/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ErrorSummary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;FormRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createDefaultRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;provideFormRuntime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@texaryn/vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SchemaEvaluationPort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;provideFormRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDefaultRegistry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ErrorSummary&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;FormRoot&lt;/span&gt; &lt;span class="na"&gt;:registry=&lt;/span&gt;&lt;span class="s"&gt;"registry"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
    &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"submission.status === 'validating' || submission.status === 'submitting'"&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"dispatch(&lt;/span&gt;{ type: 'Submit' })"
  &amp;gt;
    Submit
  &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter is created in &lt;code&gt;main.ts&lt;/code&gt; and passed as a prop, so &lt;code&gt;setup&lt;/code&gt; stays synchronous and needs no &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;. &lt;code&gt;provideFormRuntime&lt;/code&gt; takes the place of &lt;code&gt;FormProvider&lt;/code&gt;, which is why &lt;code&gt;ErrorSummary&lt;/code&gt; and &lt;code&gt;FormRoot&lt;/code&gt; sit in this component's template: they have to be descendants of the component that provided the runtime.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@texaryn/vue&lt;/code&gt; ships render functions, so single-file components are your choice rather than a requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  As a web component
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @texaryn/core @texaryn/schema-json @texaryn/web-components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createJsonSchemaAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@texaryn/schema-json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDefaultRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineTexarynForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@texaryn/web-components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TexarynFormElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@texaryn/web-components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;defineTexarynForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createJsonSchemaAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;texaryn-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TexarynFormElement&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDefaultRegistry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorSummary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;defineTexarynForm()&lt;/code&gt; registers the element explicitly: importing the package defines nothing. The element renders its own &lt;code&gt;&amp;lt;form novalidate&amp;gt;&lt;/code&gt; and turns a &lt;code&gt;submit&lt;/code&gt; event into the &lt;code&gt;Submit&lt;/code&gt; command, so a &lt;code&gt;type="submit"&lt;/code&gt; button inside it works. Set &lt;code&gt;options&lt;/code&gt; before &lt;code&gt;port&lt;/code&gt;: on an element that's already in the page, &lt;code&gt;options&lt;/code&gt; set after &lt;code&gt;port&lt;/code&gt; are ignored.&lt;/p&gt;

&lt;p&gt;The element emits &lt;code&gt;texaryn-data-change&lt;/code&gt; and &lt;code&gt;texaryn-submission-change&lt;/code&gt;. Instead of &lt;code&gt;port&lt;/code&gt; and &lt;code&gt;options&lt;/code&gt; it can take a &lt;code&gt;runtime&lt;/code&gt; you created, which it borrows and never destroys, so one runtime can render through several bindings (setting both throws). The package depends on &lt;code&gt;@texaryn/core&lt;/code&gt; only and loads no stylesheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What JSON Schema it understands
&lt;/h2&gt;

&lt;p&gt;The adapter reads Draft 7, 2019-09 and 2020-12. Draft 4 and Draft 6 aren't supported, and they aren't rejected either: a missing or unrecognized &lt;code&gt;$schema&lt;/code&gt; falls back to Draft 7, and &lt;code&gt;createJsonSchemaAdapter(schema, { defaultDialect })&lt;/code&gt; changes that fallback.&lt;/p&gt;

&lt;p&gt;Validation is delegated to &lt;code&gt;json-schema-library&lt;/code&gt;. The adapter asserts &lt;code&gt;format&lt;/code&gt; in Draft 7, which includes a schema with no &lt;code&gt;$schema&lt;/code&gt;, and never in 2019-09 or 2020-12, with no option to change that.&lt;/p&gt;

&lt;p&gt;The official JSON Schema Test Suite measures that validation, not which keywords get a form control. Mandatory tests passed by &lt;code&gt;@texaryn/schema-json&lt;/code&gt; at suite revision &lt;a href="https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/f6fd52a0a95472e079cbfc6ef7f089702b80e045" rel="noopener noreferrer"&gt;&lt;code&gt;f6fd52a&lt;/code&gt;&lt;/a&gt;, from the &lt;a href="https://texaryn.github.io/texaryn/guides/json-schema-conformance/" rel="noopener noreferrer"&gt;conformance page&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dialect&lt;/th&gt;
&lt;th&gt;Passed&lt;/th&gt;
&lt;th&gt;Mandatory tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Draft 7&lt;/td&gt;
&lt;td&gt;917&lt;/td&gt;
&lt;td&gt;929&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2019-09&lt;/td&gt;
&lt;td&gt;1,244&lt;/td&gt;
&lt;td&gt;1,261&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020-12&lt;/td&gt;
&lt;td&gt;1,278&lt;/td&gt;
&lt;td&gt;1,301&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every mandatory failure is external schema resolution: the suite expects remote documents from &lt;code&gt;http://localhost:1234&lt;/code&gt;, and the adapter has no resolver hook. It fails closed, so an unresolved reference is a &lt;code&gt;$ref&lt;/code&gt; validation error rather than a skipped check. The page deliberately gives no combined percentage.&lt;/p&gt;

&lt;p&gt;In practice, a schema that &lt;code&gt;$ref&lt;/code&gt;s another document fails validation wherever data reaches that reference, and a root that is only a remote &lt;code&gt;$ref&lt;/code&gt; makes &lt;code&gt;createFormRuntime&lt;/code&gt; throw &lt;code&gt;Schema projection missing root node&lt;/code&gt;. Bundle it into one document with local &lt;code&gt;$ref&lt;/code&gt;s first, which also work at the root.&lt;/p&gt;

&lt;p&gt;Projection has its own limits. Tuple arrays (&lt;code&gt;prefixItems&lt;/code&gt; in 2020-12, or an array-valued &lt;code&gt;items&lt;/code&gt; in Draft 7 and 2019-09) get no dedicated projection, &lt;code&gt;patternProperties&lt;/code&gt; and &lt;code&gt;additionalProperties&lt;/code&gt; generate no fields, and remote &lt;code&gt;$ref&lt;/code&gt;, &lt;code&gt;$dynamicRef&lt;/code&gt; and &lt;code&gt;$recursiveRef&lt;/code&gt; are out of scope. A &lt;code&gt;oneOf&lt;/code&gt; or &lt;code&gt;anyOf&lt;/code&gt; branch is chosen from the current data, so there's no branch picker, and schema &lt;code&gt;default&lt;/code&gt; values aren't written into the data unless you pass &lt;code&gt;initialization: 'schema-defaults'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming from RJSF
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RJSF&lt;/th&gt;
&lt;th&gt;Texaryn&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;schema&lt;/code&gt; prop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;createJsonSchemaAdapter(schema)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uiSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hints keyed by JSON Pointer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;formData&lt;/code&gt; / &lt;code&gt;onChange&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;initialData&lt;/code&gt; option, &lt;code&gt;data&lt;/code&gt; store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;schema &lt;code&gt;default&lt;/code&gt; values in the data&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;initialization: 'schema-defaults'&lt;/code&gt; (off by default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;widgets, templates, fields&lt;/td&gt;
&lt;td&gt;renderer registry, &lt;code&gt;WidgetComponent&lt;/code&gt;, &lt;code&gt;NodeRenderer&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;array rendering&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useFieldArray&lt;/code&gt; with stable item identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;liveValidate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;validationTrigger&lt;/code&gt; hints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;validate&lt;/code&gt;, &lt;code&gt;customValidate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no equivalent yet: only the schema validates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onSubmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;onSubmit&lt;/code&gt; option plus the submission lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;showErrorList&lt;/code&gt; / &lt;code&gt;ErrorList&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ErrorSummary&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transformErrors&lt;/code&gt;, &lt;code&gt;extraErrors&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no equivalent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;uiSchema: { email: { 'ui:placeholder': '...' } }&lt;/code&gt; becomes &lt;code&gt;hints: { '/email': { placeholder: '...' } }&lt;/code&gt;. &lt;code&gt;formContext&lt;/code&gt; becomes React context or props, and templates become registry entries.&lt;/p&gt;

&lt;p&gt;Adding, removing and moving rows keeps each row's identity. When &lt;code&gt;Reset&lt;/code&gt; replaces the data, rows are matched to their old identities by value, so an edited row gets a new identity and two identical rows can swap theirs, unless an &lt;code&gt;itemKey&lt;/code&gt; hint names a JSON Pointer inside each row, such as &lt;code&gt;'/id'&lt;/code&gt;, to match on instead. The &lt;a href="https://texaryn.github.io/texaryn/guides/migrating-from-rjsf/" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; covers the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What isn't there yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stability.&lt;/strong&gt; It's pre-1.0, and &lt;a href="https://www.npmjs.com/package/@texaryn/core?activeTab=versions" rel="noopener noreferrer"&gt;&lt;code&gt;@texaryn/core&lt;/code&gt;&lt;/a&gt; went from its first publish on 30 August 2026 to 0.12.1 on 23 September, so pin exact versions. These examples run with &lt;a href="https://www.npmjs.com/package/@texaryn/core/v/0.12.1" rel="noopener noreferrer"&gt;&lt;code&gt;@texaryn/core&lt;/code&gt; 0.12.1&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@texaryn/schema-json/v/0.6.3" rel="noopener noreferrer"&gt;&lt;code&gt;@texaryn/schema-json&lt;/code&gt; 0.6.3&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@texaryn/react/v/0.5.2" rel="noopener noreferrer"&gt;&lt;code&gt;@texaryn/react&lt;/code&gt; 0.5.2&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@texaryn/vue/v/0.4.2" rel="noopener noreferrer"&gt;&lt;code&gt;@texaryn/vue&lt;/code&gt; 0.4.2&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/@texaryn/web-components/v/0.4.1" rel="noopener noreferrer"&gt;&lt;code&gt;@texaryn/web-components&lt;/code&gt; 0.4.1&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation beyond the schema.&lt;/strong&gt; No custom or async validators and no server errors after validation. The runtime accepts a port whose &lt;code&gt;validate&lt;/code&gt; returns a promise, but the published adapter only validates against the schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widget sets for Vue and Web Components.&lt;/strong&gt; Bootstrap 5 and Material UI exist for React only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web component integration.&lt;/strong&gt; The element renders its own &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; in light DOM, so it can't sit inside another &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;. It has no Shadow DOM, &lt;code&gt;formAssociated&lt;/code&gt; or &lt;code&gt;ElementInternals&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI generation tooling.&lt;/strong&gt; None ships yet; it's on the roadmap. Today a generated schema and hints go through &lt;code&gt;createJsonSchemaAdapter&lt;/code&gt; and &lt;code&gt;hints&lt;/code&gt; like hand-written ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://texaryn.github.io/texaryn/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/strong&gt;, starting with &lt;a href="https://texaryn.github.io/texaryn/start/getting-started/" rel="noopener noreferrer"&gt;getting started&lt;/a&gt; and the &lt;a href="https://texaryn.github.io/texaryn/concepts/architecture/" rel="noopener noreferrer"&gt;architecture&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://texaryn.github.io/texaryn/playground/" rel="noopener noreferrer"&gt;Playground&lt;/a&gt;&lt;/strong&gt;: every catalog example through all five renderers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema details&lt;/strong&gt;: &lt;a href="https://texaryn.github.io/texaryn/guides/json-schema-support/" rel="noopener noreferrer"&gt;JSON Schema support&lt;/a&gt; and the &lt;a href="https://texaryn.github.io/texaryn/guides/json-schema-conformance/" rel="noopener noreferrer"&gt;validation test suite figures&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coming from RJSF&lt;/strong&gt;: the &lt;a href="https://texaryn.github.io/texaryn/guides/migrating-from-rjsf/" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://texaryn.github.io/texaryn/api/" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;&lt;/strong&gt;, the &lt;a href="https://www.npmjs.com/org/texaryn" rel="noopener noreferrer"&gt;packages on npm&lt;/a&gt; and &lt;a href="https://texaryn.github.io/texaryn/llms.txt" rel="noopener noreferrer"&gt;&lt;code&gt;llms.txt&lt;/code&gt;&lt;/a&gt; for coding agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/texaryn/texaryn" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/strong&gt; and the &lt;a href="https://github.com/texaryn/texaryn/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and hit a missing extension point, a confusing renderer contract or a schema that doesn't project the way you expect, &lt;a href="https://github.com/texaryn/texaryn/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://hamidihamza.com/notes/texaryn-json-schema-forms/" rel="noopener noreferrer"&gt;hamidihamza.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>vue</category>
      <category>typescript</category>
      <category>webcomponents</category>
    </item>
  </channel>
</rss>
