DEV Community

Hamza Hamidi
Hamza Hamidi

Posted on Originally published at hamidihamza.com

AJSF on Angular 22, and the way up from 0.8.0

AJSF turns a JSON Schema into an Angular form. @ajsf/core renders plain HTML, and five framework packages render it with a UI library: @ajsf/material, @ajsf/primeng, @ajsf/bootstrap3, @ajsf/bootstrap4 and @ajsf/bootstrap5.

In the week of 15 to 21 September 2026, @ajsf/core had 2,932 npm downloads, over five days of data, since npm reports none for 15 and 17 September. As npm's versions tab showed them on 24 September, 2,289 of those were version 0.8.0, from August 2022, and 216 in total were 14.0.0 or later. So this note is mostly for people still on 0.8.0: which version to move to, what changes on the way, and what a new Angular 22 app needs.

Why so many installs are still on 0.8.0

No AJSF release shipped between 0.8.0, published on 27 August 2022, and 14.0.0, published on 15 August 2026.

@ajsf/core@0.8.0 declares @angular/core >=14.0.0, an open range, so npm installs it next to any later Angular without a warning. @ajsf/material@0.8.0 also peers on @angular/flex-layout, whose matching releases require Angular 14, so on Angular 15 or later npm stops with ERESOLVE unless it runs with --legacy-peer-deps. Neither means the combination was ever built or tested.

From 14.0.0, 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 22.0.0 on 12 September 2026. The current release is 22.3.1, published on 24 September 2026.

Pick the version that matches your Angular

From 14.0.0 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 22.0.0, 22.1.0, 22.2.0, 22.3.0 and 22.3.1.

The majors that exist are 14, 16, 17, 18, 19, 20, 21 and 22. There is no release for Angular 15: 16.0.0 crossed 15 on the way from 14 to 16.

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

npm install @ajsf/core@17 @ajsf/material@17   # for Angular 17
Enter fullscreen mode Exit fullscreen mode

@ajsf/*@17 installs 17.1.0, 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, 0.8.0 or angular6-json-schema-form, so an app on an older Angular gets them by moving to Angular 22.

14.0.0 is not a rewrite: it targets the same Angular 14 as 0.8.0, and the version jump is the Angular-aligned numbering starting.

Every @ajsf release since 14.0.0 is published from the repository's release workflow with an npm provenance attestation that records the repository and workflow, and npm audit signatures verifies it.

Upgrade from 0.8.0, one major at a time

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

What a form can notice between 0.8.0 and 22

The 20.0.0, 21.0.0 and 22.0.0 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 release notes directory; for 14 to 18, read each release's page in the GitHub releases.

  • 16.0.0, Material: the widgets use Angular Material's MDC components (#367). A field without an appearance option renders as fill instead of standard. CSS written against .mat-form-field-* classes needs the .mat-mdc-* names.

  • 17.1.0, messages: validator and locale messages are corrected (#374), for example the message for a decimal multipleOf step.

  • 18.x, field validators: duplicates under uniqueItems (#379), exclusive bounds (#418) and an emptied array under minItems (#396) now mark the field, and dependencies is checked per field (#376). Decimal multipleOf steps and emoji in length limits stop showing false errors (#396). isValid and submit come from ajv, which already judged all of these cases correctly, so what changes is the message on the field.

  • 19.0.0, format: 0.8.0 already checked format with ajv 6. From 19.0.0, ajv 8 with ajv-formats is stricter, so 2021-02-30 no longer passes format: date and a@b no longer passes format: email.

  • 19.0.0, drafts: the draft is read from $schema, and a schema without it is read as draft 7, though draft 4 keywords are still converted, so a draft 4 schema without $schema 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.

  • 19.0.0, property-level required: a property-level required: true, optional or requires is dropped with a console warning naming the properties, so that field is no longer required. Move each required: true name into the parent object's required array, list there every property without optional: true, and turn requires into a dependencies entry. This is the change most likely to need a schema edit.

  • 19.0.0, references and selects: $ref against a schema's own id 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.

  • 19.0.0, counts and buttons: minProperties and maxProperties count only entered values: undefined, null and the empty string do not count, false and 0 do. A layout-declared submit button honours disableInvalidSubmit, and fixed tuple positions show no remove button.

  • 20.0.0 and 22.0.0, toolchain: moving to 20 needs Node ^20.19.0 || ^22.12.0 || >=24.0.0 (20 notes). At 22, TypeScript must be >=6.0 <6.1 and Node ^22.22.3 || ^24.15.0 || >=26.0.0 (22 notes). TypeScript 6 turns strict on when tsconfig.json does not set it, so set "strict": false or fix the new errors, and it deprecates baseUrl with TS5101: without baseUrl, each paths target needs a leading ./.

  • 20.1.0, Bootstrap 4 classes: @ajsf/bootstrap4 emits Bootstrap 4 class names instead of Bootstrap 3 ones, for example is-invalid instead of has-error. The package README tables every replacement.

  • 22.1.0, Bootstrap 4 and 5 checkboxes and radios: 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 label > input no longer matches.

  <div class="form-check">
    <input class="form-check-input" id="control1" type="checkbox">
    <label class="form-check-label" for="control1">Accept</label>
  </div>
Enter fullscreen mode Exit fullscreen mode

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 div nested inside .form-check, so a descendant selector matches every case.

  • 22.2.0, Bootstrap arrays: an array title renders once, on the <legend>, in all three Bootstrap packages. In Bootstrap 4 and 5, every field sits in a wrapper row that carries is-invalid and, for an array item, holds the remove button beside the field, so a selector such as .input-group.is-invalid no longer matches.

  • 22.3.0, Bootstrap messages: in all three Bootstrap packages, a field's validation message appears under it once the field is touched or changed (#524). Bootstrap 5 puts is-invalid on the wrapper around the field, not on the input, and the red border on a touched invalid input comes from AJSF's own .ng-invalid.ng-touched rule.

  • New packages: @ajsf/bootstrap5 is released from 18.0.0 and @ajsf/primeng from 19.2.0. Neither existed in 0.8.0.

Before each release, 74 recorded schemas are rendered in every framework, 444 cases 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.

A new Angular 22 app

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

npx @angular/cli@22 new ajsf-demo --defaults --style=css
cd ajsf-demo
npx ng add @angular/material
npm install @ajsf/material
Enter fullscreen mode Exit fullscreen mode

ng add @angular/material 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. @ajsf/material brings @ajsf/core with it; an app that already lists @ajsf/core names it in the install too, so both stay on the same version.

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

In src/app/app.ts:

import { Component, signal } from '@angular/core';
import { JsonPipe } from '@angular/common';
import { MaterialDesignFrameworkModule } from '@ajsf/material';

@Component({
  selector: 'app-root',
  imports: [MaterialDesignFrameworkModule, JsonPipe],
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {
  protected readonly schema = {
    type: 'object',
    properties: {
      name: { type: 'string', title: 'Name', minLength: 1 },
      age: { type: 'integer', title: 'Age', minimum: 18 },
      plan: { type: 'string', title: 'Plan', enum: ['free', 'pro', 'team'] },
    },
    required: ['name'],
  };

  protected readonly submitted = signal<unknown>(null);

  protected onSubmit(data: unknown): void {
    this.submitted.set(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

In src/app/app.html:

<json-schema-form
  [schema]="schema"
  framework="material-design"
  (onSubmit)="onSubmit($event)">
</json-schema-form>

<pre>{{ submitted() | json }}</pre>
Enter fullscreen mode Exit fullscreen mode

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 This field is required., and typing 16 in Age and leaving the field showed Must be 18 or more.

With valid values, Submit called the handler with:

{ "name": "Ada Lovelace", "age": 36, "plan": "pro" }
Enter fullscreen mode Exit fullscreen mode

Age arrived as a number.

The production bundle was 1.40 MB, and a new project fails the build above 1 MB. Raise the initial entry under budgets in the production configuration of angular.json:

{
  "type": "initial",
  "maximumWarning": "2MB",
  "maximumError": "2.5MB"
}
Enter fullscreen mode Exit fullscreen mode

The build also warns that ajv and ajv-formats are CommonJS. Listing them in allowedCommonJsDependencies in the build options silences the warning:

"allowedCommonJsDependencies": ["ajv", "ajv-formats"]
Enter fullscreen mode Exit fullscreen mode

The example leaves loadExternalAssets off. If your templates set it, bind it as [loadExternalAssets]="true": a project created by ng new rejects the plain attribute loadExternalAssets="true" with TS2322. 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.

Switching framework

Four things change for Bootstrap 5: the package, the module import, the framework value and the stylesheet.

npm install @ajsf/bootstrap5 bootstrap@5
Enter fullscreen mode Exit fullscreen mode
-import { MaterialDesignFrameworkModule } from '@ajsf/material';
+import { Bootstrap5FrameworkModule } from '@ajsf/bootstrap5';

-  imports: [MaterialDesignFrameworkModule, JsonPipe],
+  imports: [Bootstrap5FrameworkModule, JsonPipe],

-  framework="material-design"
+  framework="bootstrap-5"
Enter fullscreen mode Exit fullscreen mode

In angular.json, replace src/material-theme.scss in the styles array with the Bootstrap stylesheet:

"styles": [
  "node_modules/bootstrap/dist/css/bootstrap.min.css",
  "src/styles.css"
]
Enter fullscreen mode Exit fullscreen mode

The Bootstrap 5 bundle is 827 kB, which passes the default 1 MB error budget, though the build still warns above 500 kB. Bootstrap 4 works the same way with @ajsf/bootstrap4, bootstrap@4, Bootstrap4FrameworkModule and framework="bootstrap-4".

PrimeNG works the same way with PrimengFrameworkModule and framework="primeng", plus providePrimeNG with a theme preset in app.config.ts, as the @ajsf/primeng README shows. Its bundle is 1.55 MB, so it needs the raised budget too.

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 framework value. The playground also switches between them at runtime. The framework values are material-design, primeng, bootstrap-3, bootstrap-4, bootstrap-5 and no-framework, the default, which JsonSchemaFormModule from @ajsf/core renders as plain HTML.

Which JSON Schema it understands

AJSF understands draft 4, draft 6 and draft 7. Draft 4 keywords are converted whether or not a schema declares $schema, so a draft 4 schema without it keeps working.

A declared draft 4, 6 or 7 $schema is recognised over http or https and with or without the trailing #, from 22.3.1. 2019-09 and 2020-12 are not supported: a schema declaring either fails to compile, and the form does not render.

if, then and else 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. readOnly and writeOnly are accepted and have no effect.

The playground has 71 example schemas, each viewable with Material, PrimeNG, Bootstrap 3, 4 or 5, or plain HTML.

What isn't there yet

  • Support for 2019-09 and 2020-12, and a layout that follows if, then and else, both described above.
  • A single validator. JSON Schema is validated twice today, by ajv for isValid and validationErrors and by hand-written per-field validators, and the two can disagree.
  • A daisyUI package is next on the roadmap. ng-zorro and Ionic wait until users ask for them in an issue.

History and links

AJSF has continued dschnelldavis/angular2-json-schema-form in its own repository since July 2018. It first shipped as the single package angular6-json-schema-form, whose last release is 8.0.0, and later moved to the @ajsf scope. What it led to is in my note on Texaryn.

Originally published at hamidihamza.com.

Top comments (0)