npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

assert-json-body

v1.7.1

Published

OpenAPI response body extraction and JSON shape assertion toolkit (framework-agnostic)

Downloads

21,526

Readme

assert-json-body

Release npm version

Framework-agnostic toolkit to:

  • Extract OpenAPI response schemas into a compact responses.json artifact
  • Validate real JSON response bodies against the extracted required/optional field model
  • Assert inside any test runner (Vitest, Jest, Playwright, etc.)

Installation

npm install assert-json-body

Quick Start

  1. (Optional) Initialize a config file:

    npx assert-json-body config:init

    Produces assert-json-body.config.json (edit repo, spec path, output dir, etc.).

  2. Extract responses from your OpenAPI spec:

    npx assert-json-body extract

    This writes (by default):

    • ./json-body-assertions/responses.json (schema bundle)
    • ./json-body-assertions/index.ts (auto-generated typed wrapper) or the configured responsesFile for the JSON schema artifact.

    The init step also adds an npm script for convenience:

    // package.json
    {
      "scripts": {
        "responses:regenerate": "assert-json-body extract",
      },
    }

    So you can run:

    npm run responses:regenerate
  3. Validate in a test (untyped import):

    import { validateResponseShape, validateResponse } from 'assert-json-body';
    
    // Suppose you just performed an HTTP request and have jsonBody
    validateResponseShape(
      { path: '/process-instance/create', method: 'POST', status: '200' },
      jsonBody
    );
    // Throws if JSON shape violates required field presence / type rules.
    
    // Playwright convenience helper: consumes APIResponse, checks HTTP status, then validates JSON
    await validateResponse(
      { path: '/process-instance/create', method: 'POST', status: '200' },
      playwrightResponse
    );
  4. Prefer typed validation (after extract):

    import { validateResponseShape, validateResponse } from './json-body-assertions/index';
    
    // Now path/method/status are constrained to extracted endpoints
    validateResponseShape(
      { path: '/process-instance/create', method: 'POST', status: '200' },
      jsonBody
    );
    await validateResponse(
      { path: '/process-instance/create', method: 'POST', status: '200' },
      playwrightResponse
    );
    
    // @ts-expect-error invalid status not in spec
    // validateResponseShape({ path: '/process-instance/create', method: 'POST', status: '418' }, jsonBody);

Regenerate typed file whenever the spec changes by re-running extract (commit both responses.json and index.ts if you track API contract changes in version control).

You can control default throw/record behavior globally via config or env (see below) and override per call.

CI Integration

Keep the generated artifacts (responses.json, index.ts) in sync with the upstream spec during continuous integration:

Example GitHub Actions step (add after install):

	- name: Regenerate response schemas
		run: npm run responses:regenerate

If you commit the generated files:

  1. Run the regenerate step early (before tests).
  2. Add a check that the working tree is clean to ensure developers didn’t forget to re-run extraction locally:
	- name: Verify no uncommitted changes
		run: |
			git diff --exit-code || (echo 'Generated response artifacts out of date. Run: npm run responses:regenerate' && exit 1)

If you prefer not to commit generated artifacts:

  • Add the output directory (default json-body-assertions/) to .gitignore.
  • Always run npm run responses:regenerate before building / testing.

Caching tip: if your spec repo is large, you can cache the sparse checkout directory by keying on the spec ref (commit SHA) to speed up subsequent runs.

Standalone Binary (no Node.js required)

Pre-built standalone binaries are attached to each GitHub release. These are self-contained executables compiled with Deno — no Node.js or npm installation needed.

Available platforms:

  • x86_64-unknown-linux-gnu / aarch64-unknown-linux-gnu
  • x86_64-apple-darwin / aarch64-apple-darwin
  • x86_64-pc-windows-msvc

Usage in CI (e.g. GitHub Actions):

- name: Download assert-json-body
  run: |
    curl -fsSL -o assert-json-body \
      "https://github.com/camunda/assert-json-body/releases/latest/download/assert-json-body-x86_64-unknown-linux-gnu"
    chmod +x assert-json-body

- name: Regenerate response schemas
  run: ./assert-json-body extract
  env:
    AJB_REF: ${{ github.event.inputs.spec_ref || 'main' }}

To build locally (requires Deno ≥ 2.x):

npm run build:deno          # local platform
npm run build:deno:cross    # all platforms

Using the binary with a local spec file (AJB_SPEC_FILE)

When the OpenAPI spec is already available on disk — for example, in a workflow that checks out the camunda/camunda repository — you can skip the git sparse-checkout entirely by pointing at the local file with AJB_SPEC_FILE.

When specFile (or AJB_SPEC_FILE) is set, the repo, specPath, ref, and preserveCheckout options are ignored — no git operations are performed. The generated responses.json metadata will show commit: "local" instead of a SHA.

Copy-paste for camunda/camunda QA e2e workflows

The orchestration-cluster e2e test suite lives at qa/c8-orchestration-cluster-e2e-test-suite/ and already checks out the branch under test. The spec file is at zeebe/gateway-protocol/src/main/proto/v2/rest-api.yaml in the repo root. Add these steps before npm install / npm run test:

# ── Regenerate response schemas from the branch's spec ──────────
- name: Download assert-json-body binary
  run: |
    curl -fsSL -o /usr/local/bin/assert-json-body \
      "https://github.com/camunda/assert-json-body/releases/latest/download/assert-json-body-x86_64-unknown-linux-gnu"
    chmod +x /usr/local/bin/assert-json-body

- name: Regenerate response schemas from local spec
  run: assert-json-body extract
  env:
    AJB_SPEC_FILE: ${{ github.workspace }}/zeebe/gateway-protocol/src/main/proto/v2/rest-api.yaml
    AJB_OUTPUT_DIR: ${{ github.workspace }}/qa/c8-orchestration-cluster-e2e-test-suite/json-body-assertions
# ────────────────────────────────────────────────────────────────

That's it — no Node.js setup, no npm install, and no second git clone required. The binary reads the spec file that is already on disk from the workflow's checkout step and writes responses.json directly into the test suite's json-body-assertions/ directory.

Why this works: The e2e workflows (nightly matrix across stable/8.6main, on-demand branch, release) already actions/checkout the target branch. The OpenAPI spec in that checkout reflects the branch's API surface, so the generated response schemas will match the server under test.

Configuration

Config file: assert-json-body.config.json (created with npx assert-json-body config:init).

The configuration is now split into two blocks:

  1. extract: Controls OpenAPI repo checkout and artifact generation.
  2. validate: Controls global validation behaviour (recording & throw semantics).

extract block

| Field | Type | Default | Description | Env override(s) | | ------------------ | ------- | -------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------- | | repo | string | https://github.com/camunda/camunda | Git repository containing the OpenAPI spec | AJB_REPO, REPO | | specPath | string | zeebe/gateway-protocol/src/main/proto/v2/rest-api.yaml | Path to OpenAPI spec inside the repo | AJB_SPEC_PATH, SPEC_PATH | | specFile | string | — | Local spec file path (skips git checkout entirely) | AJB_SPEC_FILE | | ref | string | main | Git ref (branch/tag/sha) to checkout | AJB_REF, SPEC_REF, REF | | outputDir | string | json-body-assertions | Directory to write responses.json + generated index.ts | AJB_OUTPUT_DIR, OUTPUT_DIR | | preserveCheckout | boolean | false | Keep sparse checkout working copy (debug) | AJB_PRESERVE_CHECKOUT, PRESERVE_SPEC_CHECKOUT | | dryRun | boolean | false | Parse spec but do not write files | AJB_DRY_RUN | | logLevel | enum | info | silent error warn info debug | AJB_LOG_LEVEL | | failIfExists | boolean | false | Abort if target responses file already exists | AJB_FAIL_IF_EXISTS | | responsesFile | string | — | Optional explicit path for responses JSON (advanced) | AJB_RESPONSES_FILE, ROUTE_TEST_RESPONSES_FILE |

validate block

| Field | Type | Default | Description | Env override(s) | | ----------------------- | ------- | ------- | ----------------------------------------- | ----------------------------------------- | | recordResponses | boolean | false | Globally enable body recording | AJB_RECORD, TEST_RESPONSE_BODY_RECORD | | throwOnValidationFail | boolean | true | Throw vs structured { ok:false } result | AJB_THROW_ON_FAIL |

Additional env variables:

| Env | Purpose | | ------------------------------- | ------------------------------------------------------------------------------ | | TEST_RESPONSE_BODY_RECORD_DIR | Override directory for JSONL body recordings (default <outputDir>/recording) |

Example full config:

{
  "extract": {
    "repo": "https://github.com/camunda/camunda",
    "specPath": "zeebe/gateway-protocol/src/main/proto/v2/rest-api.yaml",
    "ref": "main",
    "outputDir": "json-body-assertions",
    "preserveCheckout": false,
    "dryRun": false,
    "logLevel": "info",
    "failIfExists": false
  },
  "validate": {
    "recordResponses": false,
    "throwOnValidationFail": true
  }
}

Notes:

  • The responses schema file defaults to <outputDir>/responses.json unless overridden.
  • Boolean env overrides accept 1|true|yes (case-insensitive).
  • Precedence per value: CLI flag > environment variable > config file > built-in default.

Schema Resolution Precedence

When validateResponseShape looks for the schema artifact, precedence is:

  1. Explicit option: responsesFilePath passed to the function
  2. Environment variable: ROUTE_TEST_RESPONSES_FILE or AJB_RESPONSES_FILE
  3. Config file: extract.responsesFile or <outputDir>/responses.json
  4. Default fallback: ./json-body-assertions/responses.json

All file issues (missing, unreadable, parse errors, malformed structure) throw clear errors.

API Reference

validateResponseShape(spec, body, options?)

Single unified API: validates body against the schema entry. Supports optional structured result mode and recording.

validateResponseShape({ path: '/foo', method: 'GET', status: '200' }, jsonBody, {
  responsesFilePath: './custom/responses.json',
  configPath: './custom-config.json',
});

Default behavior: throws on mismatch (configurable). If you pass throw:false (or set global flag) it returns a structured object:

interface ValidateResultBase {
  ok: boolean;
  errors?: string[]; // present when ok === false
  response: unknown; // the original response body you passed in
  routeContext: RouteContext; // resolved route/method/status + flattened field specs
}

Examples:

// Success (non-throw mode)
const r1 = validateResponseShape({ path: '/foo', method: 'GET', status: '200' }, body, {
  throw: false,
});
if (!r1.ok) throw new Error('unexpected');
console.log(r1.routeContext.requiredFields.map((f) => f.name));

// Failure (non-throw mode)
const r2 = validateResponseShape({ path: '/foo', method: 'GET', status: '200' }, otherBody, {
  throw: false,
});
if (!r2.ok) {
  console.warn(r2.errors); // array of error lines
  console.log(r2.routeContext.status); // resolved status used
}

Options

  • responsesFilePath / configPath – override resolution
  • throw?: boolean – override global throw setting
  • record?: boolean | { label?: string } – enable recording for this call

validateResponse(spec, playwrightResponse, options?)

Playwright-friendly wrapper: reads await response.json(), optionally enforces the expected status, then routes through validateResponseShape.

const apiResponse = await request.post('/process-instance/create', { data: payload });
await validateResponse(
  { path: '/process-instance/create', method: 'POST', status: '200' },
  apiResponse
);
  • spec.status is required when you need HTTP status enforcement; the helper throws if it does not match response.status().
  • options shares the same shape as validateResponseShape (file resolution, throw, record, config overrides).
  • Returns the same ValidateResultBase promise (use { throw:false } for structured result mode).

Nullable Fields

The validator respects the OpenAPI nullable: true property. When a field in the spec is marked nullable, null values pass validation without error. When nullable is absent or false, a null value produces a [TYPE] error:

[TYPE] /label expected string but got null

The nullable flag is extracted automatically during assert-json-body extract and stored in responses.json. You can also set it manually in a custom responses file:

{ "name": "label", "type": "string", "nullable": true }

Types

FieldSpec, RouteContext, PlaywrightAPIResponse, and other structural types are exported from @/types.

Generated Typed Entry (after extract)

After running the extractor you can import strongly-typed versions of validateResponseShape and validateResponse that constrain path, method and status to only the extracted endpoints:

import { validateResponseShape, validateResponse } from './json-body-assertions/index';

// Autocomplete + compile-time safety for path/method/status
validateResponseShape({ path: '/process-instance/create', method: 'POST', status: '200' }, body);
await validateResponse(
  { path: '/process-instance/create', method: 'POST', status: '200' },
  playwrightResponse
);

// @ts-expect-error invalid status for that route will fail type-check
// validateResponseShape({ path: '/process-instance/create', method: 'POST', status: '418' }, body);

You can also use the exported helper types:

import type { RoutePath, MethodFor, StatusFor } from './json-body-assertions/index';

type AnyRoute = RoutePath;
type GetStatus<P extends RoutePath> = StatusFor<P, MethodFor<P>>;

FAQ

Why do I get two files (responses.json and index.ts)?
responses.json is the runtime artifact used for validation. index.ts adds compile-time safety and autocomplete for route specifications.

Do I need to commit index.ts?
Recommended: yes. Changes in that file make API evolution explicit in diffs. If you prefer to ignore it, ensure your build pipeline runs assert-json-body extract first.

Can I disable type emission?
Not yet—emission is always on. Open an issue if you need a toggle.

Large spec performance concerns?
The generated index.ts only stores a nested map of route/method/status flags, not full schema trees, keeping file size modest. Extremely large specs are still typically < a few hundred KB.

How do I update types after the spec changes?
Re-run npx assert-json-body extract. The index.ts file is regenerated deterministically.

Do I still need to import from the package root?
Use the package root for generic validation utilities (validateResponseShape), and the generated ./json-body-assertions/index for strongly typed validation.

Does it support multi-file OpenAPI specs?
Yes. The extractor automatically bundles referenced files (via $ref) as long as they are within the same directory structure as the entry file.

Releasing & Versioning

This project uses semantic-release with Conventional Commits to automate:

  • Version determination (based on commit messages)
  • CHANGELOG generation (CHANGELOG.md)
  • GitHub release notes
  • npm publication

Every push to main triggers the release workflow. Ensure your commits follow Conventional Commit prefixes so changes are categorized correctly:

Common types:

  • feat: – new feature (minor release)
  • fix: – bug fix (patch release)
  • docs: – documentation only
  • refactor: – code change that neither fixes a bug nor adds a feature
  • perf: – performance improvement
  • test: – adding or correcting tests
  • chore: – build / tooling / infra

Breaking changes: add a footer line BREAKING CHANGE: <description> (or use ! after the type, e.g. feat!: drop Node 16).

Example commit message:

feat: add structured validation result for non-throw mode

BREAKING CHANGE: removed deprecated assertResponseShape in favor of unified validateResponseShape

Manual version bumps in package.json are not needed; semantic-release will handle it.

Local commit messages are validated by commitlint + husky (commit-msg hook). If a commit is rejected, adjust the prefix / format to match Conventional Commits.

CLI Commands

| Command | Description | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | assert-json-body extract | Performs sparse checkout + OpenAPI parse + response schema flattening into responses.json and emits typed index.ts. | | assert-json-body config:init | Creates a starter assert-json-body.config.json. |

Environment variables (selected):

  • ROUTE_TEST_RESPONSES_FILE / AJB_RESPONSES_FILE – override schema file
  • TEST_RESPONSE_BODY_RECORD_DIR – override recording directory (default <outputDir>/recording)
  • AJB_RECORD / TEST_RESPONSE_BODY_RECORD – set default recording on (true/1/yes)
  • AJB_THROW_ON_FAIL – set default throw behavior (true/false)

Recording (Optional)

By default, recordings are written to <outputDir>/recording (e.g. json-body-assertions/recording). Set TEST_RESPONSE_BODY_RECORD_DIR only if you want a custom location. To record responses, either:

// Per-call recording
validateResponseShape({ path: '/foo', method: 'GET', status: '200' }, body, {
  record: { label: 'GET /foo success' },
});

// Or enable globally (env): AJB_RECORD=true

Produces JSONL rows with required field list, top-level present, deep presence and body snapshot.

Integration Tests

An optional end-to-end integration test suite lives under integration/ and is excluded from the default unit test run.

Run unit tests (fast, pure):

npm test

Run integration tests (performs real OpenAPI extraction and live HTTP calls):

npm run test:integration

Local requirements:

  • Start the target service (expected at http://localhost:8080 by default), or
  • Set TEST_BASE_URL to point to a running instance

CI (Docker) example:

	- name: Start API container
		run: |
			docker run -d --name api -p 8080:8080 your/api:image
			for i in {1..30}; do curl -sf http://localhost:8080/license && break; sleep 1; done
	- name: Run integration tests
		run: npm run test:integration

If the service is unreachable, the integration test logs a warning and exits early (treated as a soft skip).

Error Messages

Errors show a capped (first 15) list of issues (missing, type, enum, extra) with JSON Pointer paths. Additional errors are summarized with a count.

Precedence Test Illustration

See src/tests/precedence.spec.ts for an executable example verifying explicit > env > config > default ordering.

License

ISC (see LICENSE).