pure-openapi-assert
v2.0.0
Published
Zero-dependency-heavy OpenAPI response assertion library for modern testing
Downloads
1,002
Maintainers
Readme
pure-openapi-assert
State-of-the-art, ultra-lightweight, and high-performance OpenAPI 3.0 response contract assertion library for modern testing.
🚀 Why pure-openapi-assert? (The AJV Problem)
Most traditional testing setups rely on AJV or other JSON Schema engines to validate API responses. However, JSON Schema is not OpenAPI. OpenAPI 3.0 introduced critical structural divergences (such as polymorphic discriminator mapping, strict nullable behaviors, and specialized numeric constraints like int32, int64, float, and double) that are either unsupported or incorrectly evaluated by standard draft-based JSON Schema validators.
pure-openapi-assert was built from the ground up to solve this mismatch, delivering:
- Zero AJV Runtime-Heavy Overhead: An independent, custom-built OpenAPI-first validation engine with zero heavy dependencies.
- Guaranteed Performance SLOs: Blazing-fast hot runs (<3ms with in-memory caching) and ultra-responsive initial cold runs (<100ms).
- Hyper-Strict TypeScript Typings: Native type safety that helps prevent mismatches in options, formats, and configurations.
- Polymorphism Support: Built-in high-performance evaluation of polymorphic compositions (
oneOf,anyOf,allOf) with explicit support fordiscriminatorrouting to avoid evaluating irrelevant schema branches. - Negation Support (
notKeyword): Full compliance with the OpenAPI 3.0notdirective. Payloads matching schemas specified under anotblock are strictly rejected, while non-matching payloads are accepted cleanly with zero error leakages or context pollution. - Comprehensive Developer Experience (DX): Validation failures are compiled into structured, highly readable nested property paths (e.g.
[body.profile.address.zipCode]) for immediate root-cause diagnosis.
Feature Comparison
| Feature / Pain Point | AJV + Custom JSON-Schema Wrappers | express-openapi-validator | pure-openapi-assert (Our Library) |
|:---|:---|:---|:---|
| Dependencies | Heavy (AJV, ajv-formats, etc.) | Bound to Express, pulls heavy HTTP middleware | Ultra-lightweight (minimal dependencies, in-memory AST router) |
| Performance | High, but heavy compilation/init times | Slowed by middleware layers & express stacks | Hot runs <3ms (via WeakMap and bounded FIFOCache) |
| Coupling | Decoupled, but requires complex glue code | Highly coupled to Express.js (unusable in Koa, Fastify, Edge) | 100% decoupling (pure in-memory AST match over raw objects) |
| HTTP Coercion | Bypassed or manual (often fails on headers/query) | Express-specific body-parsing coercion | Built-in strict coercion boundaries for headers/query |
| Floating-Point Precision| Suffers from standard V8 precision loss on large integers | Suffers from standard precision loss on large integers | Deterministic custom Lossless BigInt Parsing |
| Polymorphism | Complex schema configuration | Hard to trace dynamic discriminator paths | Explicit delegation with dispatch tables |
🛡️ Design Philosophy: Zero-Bypass Type Safety
The structural integrity of pure-openapi-assert is governed by our Zero-Bypass engineering standard:
- No Compilation Bypasses (
as any/as unknown): We ban loose type assertions. TypeScript soundness guarantees that validation contracts map cleanly at compile-time and runtime. - No Non-Null Assertions (
!): Every value that can be optional,null, orundefinedis checked explicitly using type-guards or fallback defaults. - Strict Coercion Boundaries: Wire transport values (like query parameters and headers) always arrive as strings. The library coerces them according to the OpenAPI schema into real primitive types (like
integer,boolean,number) before checking constraints, preventing silent bypasses.
⏱️ Intelligent Cache Invalidation & Watch Mode (Perfect DX)
API testing relies heavily on active Watch Mode loops (vitest --watch or jest --watch) to provide developers with instant feedback during TDD cycles. Traditionally, libraries cache OpenAPI specifications statically in memory, forcing developers to manually kill and restart their test runners every time they modify their OpenAPI specification file to see schema changes.
pure-openapi-assert natively solves this bottleneck by incorporating a Centralized State Manager with Smart Cache Invalidation based on the file modification time (mtime):
- Automatic Schema Re-parsing: Every time you run an assertion in Watch Mode, the library checks the spec file's
mtimeMson disk. If you edited and saved the spec file,pure-openapi-assertinstantly invalidates the old cache, purges all associated JSON pointer resolutions, and parses the fresh OpenAPI document on-the-fly. No test runner restarts required! - Same-Tick I/O Throttling: To satisfy our <3ms hot run SLO and prevent hammering the filesystem when executing hundreds of parallel assertions, the state manager features a microtask-buffered cache. It limits disk
statreads to exactly once per event-loop tick (queueMicrotask), ensuring supreme performance and zero disk bottlenecks.
📦 Installation
Install pure-openapi-assert via your preferred package manager:
# Using pnpm
pnpm add -D pure-openapi-assert
# Using npm
npm install --save-dev pure-openapi-assert
# Using yarn
yarn add -D pure-openapi-assert⚡ Quickstart
Validate HTTP responses directly against your OpenAPI 3.0 contract in a single line. Pass your fetch, axios, or supertest response directly to the response property—the library safely auto-extracts the status, headers, and body for you.
import { assertResponseMatchesOpenApi } from 'pure-openapi-assert';
async function testMyAPI() {
// 1. Execute your request using Fetch, Axios, or Supertest
const response = await fetch('https://api.example.com/users/123');
// 2. Assert against your OpenAPI contract instantly
await assertResponseMatchesOpenApi({
specPath: 'docs/openapi.yaml', // Path to your local OpenAPI spec file
path: '/users/{id}', // The route pattern as defined in the spec
method: 'GET',
response // Auto-extracts status, headers, and body!
});
// 3. Do your business assertions!
// (We automatically clone Fetch streams, so response.json() is still fully available to you)
const data = await response.json();
expect(data.name).toBe('Jane Doe');
}⚠️ Fetch Stream Consumption (Crucial Edge Case): Native Fetch response bodies can only be consumed once.
- Path A: Assert First (Recommended): Pass the
responseobject directly. We automatically clone it internally, keeping the original stream untouched so you can still safely callawait response.json()in your test afterwards.- Path B: Read Body First: If you already consumed the stream (e.g.
const data = await response.json()) before passing the response to the validator, the stream is spent. In this case, simply use the Explicit Mode to pass the pre-parsed body directly. You can optionally also pass theheaders(which we will automatically normalize) andcontentType:const response = await fetch('https://api.example.com/users/123'); const data = await response.json(); // <-- Stream is now consumed await assertResponseMatchesOpenApi({ specPath: 'docs/openapi.yaml', path: '/users/{id}', method: 'GET', status: response.status, body: data, // <-- Explicitly pass the pre-parsed body! headers: response.headers, // <-- Optional: Pass fetch headers directly (we normalize them) contentType: 'application/json' // <-- Optional: Explicit content type override });
📖 Complete Documentation & BDD Adoption Manual
To unlock the full potential of pure-openapi-assert, explore our comprehensive Adoption Manual. It serves as the single source of truth (SSOT) for all technical integrations and advanced configurations:
👉 pure-openapi-assert Adoption Manual & BDD Guide
What is covered in the Adoption Manual:
- The AAA (Arrange-Act-Assert) Testing Pattern — Industry standard practices for contract and BDD testing.
- Vitest Matcher Integration (
expect.extend) — Full tutorial to extend expectation runners with 100% strict type autocomplete. - Supertest & Jest Integration — Complete end-to-end setups.
- XML Response Workaround — Deep structural schema validation guidelines for XML APIs.
- Custom Format Predicates Injection — Extend and register bespoke data constraints dynamically.
- Complete OpenAPI 3.0 Keyword Compatibility Matrix — Full spec evaluation boundaries.
- Error Diagnostic Formats and Structured Remediation Guides.
🔁 Migration to 2.0.0 (nullable)
In 2.0.0, nullable: true only admits null alongside a type on the same schema (OpenAPI 3.0.3). These rules apply to every openapi: 3.0.x document, including 3.0.0–3.0.2, which left this behavior ambiguous. A null no longer passes a schema with allOf, anyOf, oneOf or $ref that does not admit it, even if the container has nullable: true. Sibling keywords of a $ref are ignored.
If your specs start failing after upgrading with Field is not nullable but received null, declare nullability on the schema that should accept null:
# Before (rejected in 2.0.0): the container is nullable, but Image does not admit null
image:
nullable: true
allOf:
- $ref: '#/components/schemas/Image'
# After: keep the $ref and add a branch that only admits null
image:
anyOf:
- $ref: '#/components/schemas/Image'
- type: object
nullable: true
enum: [null]The same applies to a $ref with a sibling nullable: true. For oneOf with two branches that admit null, leave exactly one branch nullable.
Without type, nullable has no effect: a schema with only properties and required accepts null, because those keywords only apply to objects. Declare type on schemas that must reject null. See CHANGELOG.md for the full migration guide and known limitations.
📜 License
Licensed under the ISC License. Free for personal, commercial, and enterprise usage. See the LICENSE file for details.
