@omar_kader/apis-response-validator
v1.1.6
Published
Validate API response shape: required/optional fields, types, array items, and extra fields
Maintainers
Readme
APIs Response Validator
Validate the shape of an API response: required and optional fields, types, arrays of objects, and fields you didn't declare.
- Zero dependencies, single file, CommonJS
- Framework-agnostic core (
validate), with adapters for Postman and any test runner that fails on a thrown error (Jest, Mocha, Playwright,node:test, ...) - Readable reports: missing fields, type mismatches, and extra fields, each with the exact path.
🎥 How it works
See the validator in action, catching missing fields, type mismatches, and extra fields in real-time:
Example Report Output:
❌ Missing fields (2):
- data.ongoingRideDetails.rideId
- data.communication.screenMessages[2].id
⚠️ Type mismatches (1):
- statusCode (expected number, got string)
ℹ️ Extra fields not in schema (1):
- data.debugInfo📦 Install
npm install @omar_kader/apis-response-validatorNote: In Postman, there is nothing to install via npm. See the Postman Integration section.
🚀 Quick start
const { validate, formatReport } = require('@omar_kader/apis-response-validator');
const schema = {
'error': 'boolean',
'statusCode': 'number',
'message': 'string',
'data.customerId': 'string',
'data.ongoingRideDetails.hasOngoingRide': 'boolean',
'data.ongoingRideDetails.rideId': 'string|null',
'data.communication.chat.unreadCount': 'number',
'data.communication.screenMessages': {
type: 'array',
items: {
'id': 'string',
'title': 'string',
'imageUrl': 'string|null?', // optional, may be null
'action.type': 'string'
}
},
'data.promo': 'object?' // optional
};
const body = {
error: false,
statusCode: 200,
message: 'Customer dashboard fetched successfully.',
data: {
customerId: '67f108be083a0100847f455d',
ongoingRideDetails: { hasOngoingRide: false, rideId: null },
communication: { chat: { unreadCount: 0 }, screenMessages: [] }
}
};
const result = validate(body, schema);
console.log(formatReport(result));
// ✅ All fields exist with expected typesA response with problems:
const bad = {
error: false,
statusCode: '200', // wrong type
message: 'ok',
data: {
customerId: '67f1',
debugInfo: {}, // not in schema
ongoingRideDetails: { hasOngoingRide: false }, // rideId missing
communication: {
chat: { unreadCount: 0 },
screenMessages: [
{ id: '1', title: 'Hi', action: { type: 'open' }, priority: 1 }, // priority: extra
{ id: 2, title: 'Yo', imageUrl: null, action: {} }, // id wrong type, action.type missing
{ title: 'x', action: { type: 'a' } } // id missing
]
}
}
};
console.log(formatReport(validate(bad, schema)));
// ❌ Missing fields (3):
// - data.ongoingRideDetails.rideId
// - data.communication.screenMessages[1].action.type
// - data.communication.screenMessages[2].id
// ⚠️ Type mismatches (2):
// - statusCode (expected number, got string)
// - data.communication.screenMessages[1].id (expected string, got number)
// ℹ️ Extra fields not in schema (2):
// - data.communication.screenMessages[].priority
// - data.debugInfo📖 Schema Reference
A schema is a flat object. Keys are dot paths, and values are specs.
| Spec | Meaning |
|---|---|
| 'string', 'number', 'boolean', 'object', 'array', 'null' | Required field of that type |
| 'string\|number' | Required, any one of the listed types |
| 'string?' | Optional: may be absent, but if present it must match |
| 'string\|null?' | Optional and nullable |
| 'any' or null | Required, any value (existence only) |
| 'any?' | Optional, any value |
| { type: 'array', items: {...} } | Array where every element is validated against the items schema |
| { items: {...} } | Same as above (type defaults to 'array') |
| { type: 'array?', items: {...} } | Optional array of objects |
🏷️ Types
typeOf is what the response value is measured against:
| Value | Type |
|---|---|
| "x", "" | string |
| 1, 1.5, 0 | number (integers and floats are not distinguished) |
| true, false | boolean |
| null | null (its own type, not covered by object) |
| {} | object |
| [] | array (not covered by object) |
💡 Examples by Type
Each example shows the schema, a value that passes, and one that fails.
string, number, boolean
validate({ name: 'Omar' }, { name: 'string' }); // valid
validate({ name: 1 }, { name: 'string' }); // ⚠️ name (expected string, got number)
validate({ age: 30 }, { age: 'number' }); // valid
validate({ ok: false }, { ok: 'boolean' }); // valid: false still counts as existingnull
validate({ deletedAt: null }, { deletedAt: 'null' }); // validobject and array
validate({ meta: {} }, { meta: 'object' }); // valid
validate({ tags: [] }, { tags: 'array' }); // valid
// arrays are not objects, and vice versa
validate({ tags: [] }, { tags: 'object' }); // ⚠️ tags (expected object, got array)Union types: a|b
const schema = { rideId: 'string|null', value: 'string|number' };
validate({ rideId: null, value: 'x' }, schema); // valid
validate({ rideId: 5, value: 1 }, schema); // ⚠️ rideId (expected string|null, got number)Optional fields: ?
Absent is fine. If present, the type must match.
validate({}, { nickname: 'string?' }); // valid: absent
validate({ nickname: 'Om' }, { nickname: 'string?' }); // valid
validate({ nickname: 1 }, { nickname: 'string?' }); // ⚠️ nickname (expected string, got number)null is a type. To allow it on an optional field, say so:
validate({ nickname: null }, { nickname: 'string?' }); // ⚠️ nickname (expected string, got null)
validate({ nickname: null }, { nickname: 'string|null?' }); // validExistence only: 'any' and null
validate({ a: 0, b: null, c: [] }, { a: 'any', b: null, c: 'any' }); // valid: falsy values still exist
validate({}, { a: 'any', b: null }); // ❌ Missing fields: a, b
validate({}, { a: 'any?' }); // validNested objects (dot paths)
const schema = { 'user.address.city': 'string' };
validate({ user: { address: { city: 'Giza' } } }, schema); // valid
validate({ user: {} }, schema); // ❌ user.address.city
// You can also declare the parent, and the children beneath it:
validate({ user: { id: '1' } }, { 'user': 'object', 'user.id': 'string' }); // validArrays of objects: items
Every element is checked. Failures are reported with the element index.
const schema = {
users: { type: 'array', items: { id: 'string', name: 'string' } }
};
validate({ users: [{ id: '1', name: 'A' }, { id: 2 }] }, schema);
// ❌ Missing fields (1):
// - users[1].name
// ⚠️ Type mismatches (1):
// - users[1].id (expected string, got number)
// `type` can be omitted when `items` is present:
validate({ users: [{ id: '1' }] }, { users: { items: { id: 'string' } } }); // valid
// An empty array passes (there is nothing to validate):
validate({ users: [] }, { users: { items: { id: 'string' } } }); // valid
// If the value isn't an array at all, that's a type mismatch:
validate({ users: {} }, { users: { type: 'array', items: { id: 'string' } } });
// ⚠️ users (expected array, got object)Nested arrays
items schemas support everything above, including dot paths, optional fields, and further arrays.
const schema = {
orders: {
type: 'array',
items: {
id: 'string',
'customer.email': 'string',
lines: { type: 'array', items: { sku: 'string', qty: 'number' } }
}
}
};
validate({ orders: [{ id: '1', customer: { email: '[email protected]' }, lines: [{ sku: 'a', qty: 1 }, { sku: 2 }] }] }, schema);
// ❌ Missing fields (1):
// - orders[0].lines[1].qty
// ⚠️ Type mismatches (1):
// - orders[0].lines[1].sku (expected string, got number)Root-level array responses
Schemas are matched against an object, so wrap a top-level array:
const body = [{ id: '1' }, { id: 2, extra: true }];
validate({ items: body }, { items: { type: 'array', items: { id: 'string' } } });
// ⚠️ Type mismatches (1):
// - items[1].id (expected string, got number)
// ℹ️ Extra fields not in schema (1):
// - items[].extra🛠️ API
validate(body, schema) → Result
Pure function. Never throws on validation failure.
type Result = {
valid: boolean; // no missing fields and no type mismatches (extras are ignored)
missing: string[]; // e.g. 'data.customerId', 'users[1].name'
wrongType: string[]; // e.g. 'statusCode (expected number, got string)'
extra: string[]; // e.g. 'data.debugInfo', 'users[].nickname'
};formatReport(result) → string
Human-readable report, as shown throughout this README.
assertValid(body, schema) → Result
Returns the result if valid. Otherwise throws an Error whose message is the report and whose err.result is the Result. Use it with any test runner.
try {
assertValid({ a: 1 }, { a: 'string', b: 'number' });
} catch (err) {
console.log(err.message);
// Response validation failed
// ❌ Missing fields (1):
// - b
// ⚠️ Type mismatches (1):
// - a (expected string, got number)
console.log(err.result.missing); // ['b']
}createPostmanAsserter(pm) → { assertFields }
Postman adapter. pm is passed in explicitly. See Postman Integration.
🏃 Usage with Test Runners
assertValid throws, so it works anywhere an exception fails the test.
Node script (Node 18+ for global fetch)
const { validate, formatReport } = require('@omar_kader/apis-response-validator');
const res = await fetch('https://api.example.com/dashboard');
const result = validate(await res.json(), schema);
if (!result.valid) {
console.error(formatReport(result));
process.exit(1);
}Jest / Mocha / node:test
const { assertValid } = require('@omar_kader/apis-response-validator');
it('returns the customer dashboard', async () => {
const body = await (await fetch(url)).json();
assertValid(body, schema); // fails the test with the full report
});With Supertest
const res = await request(app).get('/dashboard').expect(200);
assertValid(res.body, schema);Playwright
import { test } from '@playwright/test';
import { assertValid } from '@omar_kader/apis-response-validator';
test('dashboard shape', async ({ request }) => {
const res = await request.get('/dashboard');
assertValid(await res.json(), schema);
});📮 Postman Integration
Postman can import public npm packages in scripts with pm.require.
Basic usage (Tests tab)
const { createPostmanAsserter } = pm.require('npm:@omar_kader/[email protected]');
const { assertFields } = createPostmanAsserter(pm);
pm.test('Status code is 200', () => pm.response.to.have.status(200));
assertFields(pm.response.json(), {
'error': 'boolean',
'statusCode': 'number',
'message': 'string',
'data.customerId': 'string',
'data.ongoingRideDetails.rideId': 'string|null',
'data.communication.screenMessages': {
type: 'array',
items: { 'id': 'string', 'imageUrl': 'string|null?' }
}
});assertFields behavior:
- Prints the report to the Postman Console (View → Show Postman Console)
- Registers two tests: All required fields exist and All fields have expected types
- Never throws, so the rest of your script still runs
- Returns the same
Resultobject asvalidate
Custom test with validate
const { validate, formatReport } = pm.require('npm:@omar_kader/[email protected]');
const result = validate(pm.response.json(), schema);
pm.test('Response shape is valid', () => {
pm.expect(result.valid, formatReport(result)).to.be.true;
});Postman Notes
- Pin an exact version (
@1.0.0). Postman doesn't support version ranges or tags. If you omit the version, the latest one is used, tracked per Postman app. - Newman does not support external packages. Postman's docs list the Collection Runner, monitors, and the Postman CLI as supported, and Newman as unsupported.
- Public npm packages are importable on any plan. See Postman's docs on external registries.
⚠️ Rules and Limitations
- Existence means the key is present (own property).
null,0,false,'', and[]all count as existing. Inherited keys likeconstructordo not. nullis its own type. A nullable field needs|null.- Numbers: integers and floats are both
number. - Unknown type names never match. A typo like
'str'is reported asa (expected str, got string). - Keys that contain a literal
.are not supported, because.is the path separator. itemsvalidates objects only. An array of primitives (['a', 'b']) can only be checked as'array'. If you useitemson one, each element is reported as(expected object, got string).- Empty arrays pass and validate nothing.
- Leaf
'object'and'array'values are opaque. Their contents are neither validated nor reported as extras unless you declare child paths (oritems). - Report paths: missing and type-mismatch paths include the element index (
users[2].id). Extra-field paths collapse indexes to[]and are deduplicated. - A non-object
body(null, a string, ...) reports every required field as missing.
📄 License
MIT LICENSE
