perfect-payload
v2.0.4
Published
Lightweight JSON payload validation library with structured errors, nested validation, field paths, and customizable validation rules.
Maintainers
Readme
perfect-payload
A lightweight JavaScript library for validating, transforming, and sanitizing API and JSON payloads. perfect-payload provides structured validation errors, exact nested field paths, synchronous and asynchronous custom validation, transformations, nested object/array validation, unknown-field handling, and ready-to-use Express and Fastify integrations. The package is designed to stay simple and lightweight, with no Express or Fastify runtime dependency.
Highlights
- Lightweight, rule-based payload validation
- TypeScript-first package with built-in declarations — no separate
@types/perfect-payload - Structured errors with stable machine-readable error codes
- Exact nested paths such as
profile.emailandproducts[1].quantity - Recursive
objectAttrandelementConstraintsvalidation - Array constraints with
minItemsandmaxItems - Built-in
trim,lowercase, anduppercasetransformations - Custom synchronous
transform(value, payload) - Array-element transforms with
transform(value, index, payload) - Deterministic validation precedence independent of rule-property order
- Synchronous custom validators with
perfectPayload() - Synchronous or asynchronous custom validators with
perfectPayloadAsync() - Configurable unknown-field handling:
strip,allow, orreject - Optional simplified errors with
prettyErrors - Express middleware integration
- Fastify hook integration
- Validate
headers,params,query, andbody - Aggregated validation errors across request sources
- Framework-aware error paths such as
body.emailandparams.userId - Transformed values returned through
validatedPayload - Original input payload/request data is not mutated
- Zero Express or Fastify runtime dependency
- Legacy
perfectPayloadV1()retained during the migration period
Quick Links
- Installation
- Basic Usage
- Public API
- TypeScript
- Options
- Pretty Errors
- Express Integration
- Fastify Integration
- Framework Request Validation
- Unknown Field Handling
- Synchronous vs Asynchronous Validation
- Validation Rules
- Validation Precedence
- Array Size and Nested Validation
- Transformations and Sanitization
- Custom Validators
- Asynchronous Validation
- Error Codes
- Custom Error Messages
- Nested Objects and Array Field Paths
- Examples and Usage
- Legacy API
What's New in v2.0.0
v2.0.0 establishes the stable TypeScript-first architecture for perfect-payload while keeping the package lightweight and framework-independent.
Major v2 additions and hardening include:
- Built-in TypeScript declarations generated from the source — no separate
@types/perfect-payloadpackage. - Public validation types such as
ValidationRulesandValidationOptions. - Structured validation errors with stable
path,code, andmessagefields. prettyErrorsfor consumers that prefer a simple string array.- Recursive unknown-field handling with
strip,allow, andreject. - Exact nested and indexed paths across
objectAttrandelementConstraints. - Synchronous transforms with a complete root-payload context.
- Array-element transforms with
(value, index, payload). - Deterministic validation precedence that does not depend on the order in which rule properties are written.
perfectPayloadAsync()with asynccustomValidatorsupport and sync-error pruning.- Lightweight Express and Fastify adapters for
headers,params,query, andbody. - Framework-aware generated error paths/messages while preserving custom error messages exactly as configured.
- Legacy
perfectPayloadV1()retained during the migration period.
Express Integration
perfect-payload provides a lightweight Express adapter so request validation can be added directly as middleware. Express is not installed as a dependency of perfect-payload.
Import
import { validatePayload, validatePayloadAsync } from "perfect-payload/express";Validate Request Body
Use validatePayload() when your validation rules are synchronous.
import express from "express";
import { validatePayload } from "perfect-payload/express";
const app = express();
app.use(express.json());
const userRules = {
email: {
mandatory: true,
type: "email",
trim: true,
lowercase: true,
},
age: {
mandatory: true,
type: "number",
min: 18,
},
};
app.post(
"/users",
validatePayload({
rule: {
body: userRules,
},
}),
(req, res) => {
const user = req.validatedPayload.body;
res.json({
message: "User created",
user,
});
},
);When validation succeeds, the middleware calls next() and makes the processed payload available at:
req.validatedPayload;For the example above:
req.validatedPayload = {
body: {
email: "[email protected]",
age: 29,
},
};The original req.body is not mutated.
Validate Multiple Request Sources
The adapter can validate headers, params, query, and body together.
app.post(
"/users/:userId",
validatePayload({
rule: {
headers: {
authorization: {
mandatory: true,
type: "string",
},
},
params: {
userId: {
mandatory: true,
type: "string",
},
},
query: {
notify: {
type: "boolean",
},
},
body: {
email: {
mandatory: true,
type: "email",
trim: true,
lowercase: true,
},
},
},
}),
(req, res) => {
const { headers, params, query, body } = req.validatedPayload;
res.json({
headers,
params,
query,
body,
});
},
);Only request sources configured inside rule are included in req.validatedPayload.
Validation Errors
All configured request sources are validated and their errors are aggregated.
Structured error paths include the request source:
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "body.email",
code: "INVALID_EMAIL",
message: "Invalid email format for attribute body.email"
}
]
}For framework-generated default errors, the request source is part of the generated field path/message. Custom messages such as mandatoryError, typeError, regexError, and customValidatorError are returned exactly as configured and are not prefixed or rewritten by the adapter.
Adapter Options
Core options can be passed through the adapter using options:
validatePayload({
rule: {
body: userRules,
},
options: {
unknownFields: "reject",
prettyErrors: false,
inValidPayloadResponse: {
statusCode: 422,
valid: false,
message: "Request validation failed",
},
},
});Async Validation
Use validatePayloadAsync() when the schema contains asynchronous customValidator functions.
import { validatePayloadAsync } from "perfect-payload/express";
app.post(
"/users",
validatePayloadAsync({
rule: {
body: {
username: {
mandatory: true,
type: "string",
trim: true,
customValidator: async (value) => {
return await isUsernameAvailable(value);
},
customValidatorCode: "USERNAME_TAKEN",
customValidatorError: "Username is already taken",
},
},
},
}),
(req, res) => {
res.json(req.validatedPayload.body);
},
);Use:
validatePayload()for synchronous validation.validatePayloadAsync()when asynchronouscustomValidatorfunctions are required.
Validation failures are handled by the middleware automatically. Unexpected errors from validators, transformations, or configuration are passed to Express through next(error).
Fastify Integration
perfect-payload provides a lightweight Fastify adapter that can be used directly as a route hook. Fastify is not installed as a dependency of perfect-payload.
Import
import { validatePayload, validatePayloadAsync } from "perfect-payload/fastify";Validate Request Body
Use the adapter as a Fastify preValidation hook:
import Fastify from "fastify";
import { validatePayload } from "perfect-payload/fastify";
const fastify = Fastify();
const userRules = {
email: {
mandatory: true,
type: "email",
trim: true,
lowercase: true,
},
age: {
mandatory: true,
type: "number",
min: 18,
},
};
fastify.post(
"/users",
{
preValidation: validatePayload({
rule: {
body: userRules,
},
}),
},
async (request, reply) => {
const user = request.validatedPayload.body;
return {
message: "User created",
user,
};
},
);When validation succeeds, the processed payload is available at:
request.validatedPayload;For the example above:
request.validatedPayload = {
body: {
email: "[email protected]",
age: 29,
},
};The original request.body is not mutated.
Validate Multiple Request Sources
The adapter can validate headers, params, query, and body together.
fastify.post(
"/users/:userId",
{
preValidation: validatePayload({
rule: {
headers: {
authorization: {
mandatory: true,
type: "string",
},
},
params: {
userId: {
mandatory: true,
type: "string",
},
},
query: {
notify: {
type: "boolean",
},
},
body: {
email: {
mandatory: true,
type: "email",
trim: true,
lowercase: true,
},
},
},
}),
},
async (request, reply) => {
const { headers, params, query, body } = request.validatedPayload;
return {
headers,
params,
query,
body,
};
},
);Only request sources configured inside rule are included in request.validatedPayload.
Validation Errors
All configured request sources are validated and their errors are aggregated.
Structured error paths include the request source:
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "body.email",
code: "INVALID_EMAIL",
message: "Invalid email format for attribute body.email"
}
]
}For framework-generated default errors, the request source is part of the generated field path/message. Custom messages such as mandatoryError, typeError, regexError, and customValidatorError are returned exactly as configured and are not prefixed or rewritten by the adapter.
Adapter Options
Core options can be passed through the adapter using options:
validatePayload({
rule: {
body: userRules,
},
options: {
unknownFields: "reject",
prettyErrors: false,
inValidPayloadResponse: {
statusCode: 422,
valid: false,
message: "Request validation failed",
},
},
});Async Validation
Use validatePayloadAsync() when the schema contains asynchronous customValidator functions.
import { validatePayloadAsync } from "perfect-payload/fastify";
fastify.post(
"/users",
{
preValidation: validatePayloadAsync({
rule: {
body: {
username: {
mandatory: true,
type: "string",
trim: true,
customValidator: async (value) => {
return await isUsernameAvailable(value);
},
customValidatorCode: "USERNAME_TAKEN",
customValidatorError: "Username is already taken",
},
},
},
}),
},
async (request, reply) => {
return request.validatedPayload.body;
},
);Use:
validatePayload()for synchronous validation.validatePayloadAsync()when asynchronouscustomValidatorfunctions are required.
Validation failures are handled by the hook automatically. Unexpected errors from validators, transformations, or configuration propagate through Fastify's normal error-handling lifecycle.
Framework-aware error paths
Structured framework validation errors include the request source:
{
path: "body.email",
code: "INVALID_EMAIL",
message: "Invalid email format for attribute body.email"
}Generated default errors use source-aware paths such as:
headers.x-request-id
params.userId
query.page
body.emailA generated pretty error can therefore look like:
Custom validation failed for attribute headers.x-test-idCustom developer-defined messages are preserved exactly. For example:
customValidatorError: "Invalid x-request-id in headers";returns:
Invalid x-request-id in headersThe framework adapter does not prepend headers., params., query., or body. to a custom message.
prettyErrors
prettyErrors provides a simplified string-error mode. Structured errors remain the default.
const result = perfectPayload(payload, rules, {
prettyErrors: true,
});With prettyErrors: true, the errors array contains human-readable message strings instead of structured error objects. prettyErrors is also supported by perfectPayloadAsync() and the Express/Fastify integrations.
Error privacy
Default validation messages do not include submitted payload values. Schema constraints such as allowed enum values, minimums, maximums, and ranges may still appear in validation messages. Custom error messages are controlled by the application and are returned as configured.
Lightweight framework integrations
Express and Fastify are not installed as dependencies of perfect-payload. The framework integrations are thin adapters around the same validation
engine used by:
perfectPayload();
perfectPayloadAsync();This keeps the package lightweight while allowing framework users to integrate validation without writing their own middleware or hooks.
Installation
npm install perfect-payloadBasic Usage
Use perfectPayload() for all new implementations.
import { perfectPayload } from "perfect-payload";
const payload = {
name: "Kiran",
email: "[email protected]",
age: 29,
};
const validationRules = {
name: {
mandatory: true,
type: "string",
},
email: {
mandatory: true,
type: "email",
},
age: {
mandatory: true,
type: "number",
min: 18,
},
};
const result = perfectPayload(payload, validationRules);
console.log(result);Valid Response
{
statusCode: 200,
valid: true,
validatedPayload: {
name: "Kiran",
email: "[email protected]",
age: 29
}
}By default, validatedPayload contains only fields defined in the validation schema. Extra payload fields are stripped unless unknownFields is explicitly configured as "allow" or "reject". The original input payload is not mutated. (such as validatedBody, sanitisedData or parsedBody).
Invalid Response
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "email",
code: "INVALID_EMAIL",
message: "Invalid email format for attribute email"
}
]
}Each error returned by perfectPayload() contains:
{
path: "field.path",
code: "ERROR_CODE",
message: "Human readable validation message"
}pathidentifies the exact field that failed validation.codeprovides a stable machine-readable validation error code.messageprovides a human-readable description of the validation
failure.
- Submitted payload values are not included in default error messages.
Public API
For v2 and all new implementations, both supported APIs use the same clean three-argument signature:
perfectPayload(data, validationRules, options?)
await perfectPayloadAsync(data, validationRules, options?)The arguments are:
| Argument | Required | Description |
| ----------------- | -------- | ----------------------------------------------------------------------------------- |
| data | No | Payload/object to validate. Defaults to{}. |
| validationRules | No | Validation schema. Defaults to{}. |
| options | No | API-level configuration such as unknown-field handling and custom response objects. |
The third argument is a single options object. You no longer need to pass separate positional arguments for custom valid and invalid responses. In TypeScript, this object is typed as ValidationOptions.
TypeScript
perfect-payload ships its TypeScript declarations with the package. No separate @types/perfect-payload installation is required.
import { perfectPayload, perfectPayloadAsync } from "perfect-payload";
import type {
ValidationOptions,
ValidationRules,
AttributeValidationRules,
ValidationValueType,
TransformFunction,
ArrayElementTransformFunction,
CustomValidator,
DependencyRule,
DependencyRules,
} from "perfect-payload";Example:
const rules: ValidationRules = {
email: {
mandatory: true,
type: "email",
trim: true,
lowercase: true,
},
};
const options: ValidationOptions = {
unknownFields: "reject",
prettyErrors: false,
};
const result = perfectPayload(
{
email: " [email protected] ",
},
rules,
options,
);
if (result.valid === true) {
console.log(result.validatedPayload);
} else {
console.log(result.errors);
}prettyErrors: true changes the error collection to strings, while the default structured mode returns objects containing path, code, and message. The runtime package remains the same for JavaScript and TypeScript consumers.
Options
The third argument uses the ValidationOptions type in TypeScript.
interface ValidationOptions {
unknownFields?: "strip" | "allow" | "reject";
prettyErrors?: boolean;
validPayloadResponse?: {
statusCode: number;
valid: true;
[key: string]: unknown;
};
inValidPayloadResponse?: {
statusCode: number;
valid: false;
message: string;
[key: string]: unknown;
};
}All properties are optional. The defaults are equivalent to:
{
unknownFields: "strip",
prettyErrors: false,
validPayloadResponse: {
statusCode: 200,
valid: true,
},
inValidPayloadResponse: {
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
},
}Example:
const result = perfectPayload(payload, validationRules, {
unknownFields: "reject",
prettyErrors: false,
validPayloadResponse: {
statusCode: 201,
valid: true,
message: "Payload accepted",
},
inValidPayloadResponse: {
statusCode: 422,
valid: false,
message: "Payload validation failed",
},
});The same options object is supported by perfectPayloadAsync():
const result = await perfectPayloadAsync(payload, validationRules, {
unknownFields: "reject",
prettyErrors: false,
inValidPayloadResponse: {
statusCode: 422,
valid: false,
message: "Payload validation failed",
},
});Unknown Field Handling
unknownFields controls what happens when the input payload contains a field that is not defined in the validation schema.
Supported values:
| Value | Behavior |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| "strip" | Removes unknown fields fromvalidatedPayload. This is the default and preserves the existing behavior. |
| "allow" | Preserves unknown fields invalidatedPayload. |
| "reject" | Rejects unknown fields with structuredUNKNOWN_FIELD validation errors. |
strip — default
const payload = {
name: "Kiran",
role: "developer",
};
const rules = {
name: {
type: "string",
},
};
const result = perfectPayload(payload, rules);Result:
{
statusCode: 200,
valid: true,
validatedPayload: {
name: "Kiran"
}
}role is not part of the schema, so it is removed from validatedPayload.
You can also set the default behavior explicitly:
perfectPayload(payload, rules, {
unknownFields: "strip",
});allow — preserve unknown fields
const result = perfectPayload(payload, rules, {
unknownFields: "allow",
});Result:
{
statusCode: 200,
valid: true,
validatedPayload: {
name: "Kiran",
role: "developer"
}
}Schema-defined fields are still validated normally. Unknown fields are simply preserved.
reject — reject unknown fields
const result = perfectPayload(payload, rules, {
unknownFields: "reject",
});Result:
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "role",
code: "UNKNOWN_FIELD",
message: "Unknown field role is not allowed"
}
]
}Unknown-field errors use the same structured error format as all other validation errors.
Nested objects
Unknown-field handling is recursive for schemas using objectAttr.
const payload = {
profile: {
city: "Bengaluru",
role: "developer",
},
};
const rules = {
profile: {
type: "object",
objectAttr: {
city: {
type: "string",
},
},
},
};
const result = perfectPayload(payload, rules, {
unknownFields: "reject",
});Returns:
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "profile.role",
code: "UNKNOWN_FIELD",
message: "Unknown field profile.role is not allowed"
}
]
}Arrays and deep paths
unknownFields also applies recursively through elementConstraints. For an unknown field inside an array element, the error path includes
the array index:
{
path: "products[0].internalId",
code: "UNKNOWN_FIELD",
message: "Unknown field products[0].internalId is not allowed"
}This continues through deeply nested combinations of objects and arrays,
for example:
profile.teams[0].members[0].roleNormal validation errors and unknown fields
With "reject", unknown-field errors can be returned together with normal validation errors.
For example, an invalid email plus two unknown fields can produce:
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "email",
code: "INVALID_EMAIL",
message: "Invalid email format for attribute email"
},
{
path: "role",
code: "UNKNOWN_FIELD",
message: "Unknown field role is not allowed"
},
{
path: "active",
code: "UNKNOWN_FIELD",
message: "Unknown field active is not allowed"
}
]
}With "allow", unknown fields do not create validation errors. Normal schema validation continues unchanged.
Async behavior
perfectPayloadAsync() supports the same unknownFields option:
const result = await perfectPayloadAsync(payload, rules, {
unknownFields: "reject",
});Unknown-field checking is part of the synchronous validation phase. If "reject" finds an unknown field, asynchronous customValidator functions are not executed for that payload. This follows the normal two-phase contract of perfectPayloadAsync().
Own properties only
Unknown-field handling considers only the payload object's own enumerable properties. Enumerable properties inherited through the prototype chain are ignored.
Invalid option values
Only these values are accepted:
strip
allow
rejectAny other value throws a configuration error:
TypeError[ERR_INVALID_UNKNOWN_FIELDS]: unknownFields must be one of "strip", "allow", or "reject"This is a configuration error, not a payload validation error.
Synchronous vs Asynchronous Validation
For normal synchronous validation, use perfectPayload():
import { perfectPayload } from "perfect-payload";
const result = perfectPayload(payload, validationRules, options);When any customValidator needs to perform asynchronous work, use perfectPayloadAsync() and await the result:
import { perfectPayloadAsync } from "perfect-payload";
const result = await perfectPayloadAsync(payload, validationRules, options);The public APIs are:
perfectPayloadV1() legacy API; deprecated
perfectPayload(data, rules, options?) synchronous validation
perfectPayloadAsync(data, rules, options?) synchronous + asynchronous
customValidatorperfectPayload() remains synchronous and intentionally rejects a customValidator that returns a Promise. This preserves the existing synchronous API contract. perfectPayloadAsync() first performs transformations and normal synchronous validation. If synchronous validation fails, the result is returned immediately and asynchronous validators are not executed. This avoids unnecessary asynchronous work for payloads that are already invalid.
transformations
↓
synchronous validation
↓
sync errors? ── yes ──→ return validation errors
↓ no
async customValidator
↓
return resultLegacy API
perfectPayloadV1() is still available for backward compatibility.
import { perfectPayloadV1 } from "perfect-payload";perfectPayloadV1() is deprecated and will no longer be supported after March 31, 2027. Existing applications can continue using it during the migration period,
but all new implementations should use the current API:
perfectPayload(data, validationRules, options?);For asynchronous custom validation:
await perfectPayloadAsync(data, validationRules, options?);The legacy API continues to return validation errors as:
errors: ["email is invalid"];while the new perfectPayload() API returns structured errors:
errors: [
{
path: "email",
code: "INVALID_EMAIL",
message: "Invalid email format for attribute email",
},
];Note: If an inValidPayloadResponse is provided in the options, the system returns it alongside an automatically generated errors property. Do not include your own errors attribute inside the custom options.inValidPayloadResponse object.
Validation Rules
perfectPayload() supports validation, nested-schema, custom-validation, and transformation rules.
mandatory
Marks a field as required. An empty string is also treated as missing. Default: false, the field is not required.
const rules = {
name: {
mandatory: true,
},
};Error code: REQUIRED
allowNull
Controls whether null values are accepted. Default: true, null values are allowed.
Example:
const rules = {
name: {
allowNull: false,
},
};Error code: NULL_NOT_ALLOWED
allowEmptyObject
Controls whether an empty object {} is accepted. Default: true, empty objects are allowed.
Example:
const rules = {
address: {
type: "object",
allowEmptyObject: false,
},
};Error code: EMPTY_OBJECT_NOT_ALLOWED
allowEmptyArray
Controls whether an empty array [] is accepted. Default: true, empty arrays are allowed.
Example:
const rules = {
products: {
type: "array",
allowEmptyArray: false,
},
};Error code: EMPTY_ARRAY_NOT_ALLOWED
minItems
Defines the minimum number of items required in an array. Default: Not applied when omitted.
const rules = {
tags: {
type: "array",
minItems: 2,
},
};An array with fewer than 2 items returns MIN_ITEMS.
{
path: "tags",
code: "MIN_ITEMS",
message: "Attribute tags must contain at least 2 item(s)"
}minItems is enforced even when allowEmptyArray: true is set. For example, minItems: 2 still rejects []. Error code: MIN_ITEMS
maxItems
Defines the maximum number of items allowed in an array. Default: Not applied when omitted.
const rules = {
tags: {
type: "array",
maxItems: 5,
},
};An array with more than 5 items returns MAX_ITEMS.
{
path: "tags",
code: "MAX_ITEMS",
message: "Attribute tags must contain at most 5 item(s)"
}minItems and maxItems can be used together.
Error code: MAX_ITEMS
type
Validates the expected data type.
Supported values:
number
string
boolean
email
url
enum
uuid
uuidv1
uuidv3
uuidv4
uuidv5
objectId
array
objectExample:
const rules = {
age: {
type: "number",
},
email: {
type: "email",
},
active: {
type: "boolean",
},
};enum
A specific set of allowed values (supports heterogeneous arrays)
type: "enum";Example:
const rules = {
status: {
type: "enum",
enumValues: ["active", "inactive", "blocked", 1, 0],
},
};enumValues
Used together with:
type: "enum";Example:
const rules = {
status: {
type: "enum",
enumValues: ["active", "inactive", "blocked"],
},
};Error code: INVALID_ENUM
Possible error codes for types:
INVALID_TYPE
INVALID_EMAIL
INVALID_URL
INVALID_ENUM
INVALID_UUID
INVALID_UUID_V1
INVALID_UUID_V3
INVALID_UUID_V4
INVALID_UUID_V5
INVALID_OBJECT_IDFor type: "number", NaN is rejected as INVALID_TYPE.
regex
Validates a value using a regular expression. Default: Not applied when omitted.
Example:
const rules = {
employeeCode: {
type: "string",
regex: /[^1]{3}[0-9]{3}$/,
},
};Error code: REGEX_MISMATCH
minLength
Defines the minimum allowed string length. Default: Not applied when omitted.
Example:
const rules = {
username: {
type: "string",
minLength: 5,
},
};Error code: MIN_LENGTH
maxLength
Defines the maximum allowed string length. Default: Not applied when omitted.
Example:
const rules = {
username: {
type: "string",
maxLength: 20,
},
};Error code: MAX_LENGTH
preventDecimal
Prevents decimal numbers. Default: false; both integer and decimal numbers are allowed.
Example:
const rules = {
quantity: {
type: "number",
preventDecimal: true,
},
};Error code: DECIMAL_NOT_ALLOWED
min
Defines the minimum allowed numeric value. Default: Not applied when omitted.
Example:
const rules = {
age: {
type: "number",
min: 18,
},
};Error code: MIN_VALUE
max
Defines the maximum allowed numeric value. Default: Not applied when omitted.
Example:
const rules = {
quantity: {
type: "number",
max: 100,
},
};Error code: MAX_VALUE
range
Defines the allowed numeric range.
Default: Not applied when omitted.
const rules = {
marks: {
type: "number",
range: "-20:100",
},
};Range boundaries are order-independent. For example, these are equivalent:
-100:100
100:-100range supports both number values and numeric string values.
const payload = {
marks: "-22",
};
const rules = {
marks: {
type: "string",
range: "-100:100",
},
};Numeric strings are converted only for range comparison. The original value type is preserved in validatedPayload.
validatedPayload.marks;
// "-22"Legacy - separator
The previous - separator remains supported for backward compatibility:
0-100
-20-100Support for the legacy - separator is planned to be removed after March 31, 2027.
Error code: OUT_OF_RANGE
elementConstraints
Validates every item in an array.
Example:
const rules = {
marks: {
type: "array",
elementConstraints: {
type: "number",
range: "0-100",
},
},
};Example error:
{
path: "marks[2]",
code: "OUT_OF_RANGE",
message: "Attribute marks[2] should have a value between 0 and 100"
}Array-element transformations receive three arguments:
const rules = {
tags: {
type: "array",
elementConstraints: {
transform: (value, index, payload) => {
return String(value).trim().toLowerCase();
},
type: "string",
},
},
};For an elementConstraints transform:
valueis the current array element.indexis the zero-based index of that element.payloadis the complete root payload/source object currently being validated.
The same root payload object is provided even when elementConstraints is nested inside another object or array. When elementConstraintsError is explicitly provided, the error code is INVALID_ARRAY_ELEMENT.
Example:
const rules = {
marks: {
type: "array",
elementConstraints: {
type: "number",
},
elementConstraintsError: "Every marks element must be a number",
},
};objectAttr
Validates fields inside a nested object.
Example:
const rules = {
address: {
type: "object",
objectAttr: {
city: {
mandatory: true,
type: "string",
},
location: {
type: "object",
objectAttr: {
latitude: {
type: "number",
},
longitude: {
type: "number",
},
},
},
},
},
};Nested errors include the complete field path:
{
path: "address.location.latitude",
code: "INVALID_TYPE",
message:
"Invalid type for attribute address.location.latitude, required number value"
}dependency
Allows validation rules to depend on another field.
Example:
const rules = {
minSalary: {
type: "number",
dependency: {
maxSalary: {
setDependencyRule: (minSalary, maxSalary) => ({
type: "number",
min: minSalary + 1,
minError: "maxSalary must be more than minSalary",
}),
},
},
},
};Example error:
{
path: "maxSalary",
code: "MIN_VALUE",
message:
"maxSalary must be more than minSalary"
}Validation Precedence
v2 uses a fixed validation order. The order in which properties are written inside a rule object does not change runtime behavior.
1. mandatory / presence
2. transformations
trim
lowercase
uppercase
transform
3. allowNull
4. type
5. empty-container checks
allowEmptyObject
allowEmptyArray
6. structural rules
objectAttr
elementConstraints
minItems
maxItems
7. value constraints
regex
minLength
maxLength
preventDecimal
min
max
range
8. dependency
9. customValidatorFor a single field, once an earlier validation fails, later validations for that field are pruned.
Examples:
mandatory fails
→ transform/type/customValidator do not run
type fails
→ objectAttr/elementConstraints/regex/customValidator do not run
regex fails
→ customValidator does not runThis behavior applies to the modern core API and therefore also to the Express and Fastify adapters. perfectPayloadV1() keeps its legacy behavior. perfectPayloadAsync() follows the same synchronous precedence first. Async custom validators run only when the synchronous phase completes without validation errors.
Array Size and Nested Validation
perfectPayload() supports array size constraints and recursive validation of arrays and objects at multiple depths. Array indexes and nested object keys are preserved in structured error paths.
Array size constraints
Use minItems and maxItems with type: "array":
const rules = {
products: {
type: "array",
minItems: 1,
maxItems: 3,
elementConstraints: {
type: "object",
objectAttr: {
productId: { mandatory: true, type: "string" },
quantity: { mandatory: true, type: "number", min: 1 },
},
},
},
};If the array is empty, minItems reports the array path itself:
{
path: "products",
code: "MIN_ITEMS",
message: "Attribute products must contain at least 1 item(s)"
}Arrays of objects
elementConstraints can contain objectAttr, allowing every object in an array to use a nested schema. An invalid quantity in the second product is reported as:
products[1].quantityDeeply nested arrays and objects
objectAttr and elementConstraints can be combined recursively:
const rules = {
orders: {
type: "array",
minItems: 1,
maxItems: 2,
elementConstraints: {
type: "object",
objectAttr: {
orderId: { mandatory: true, type: "string" },
items: {
mandatory: true,
type: "array",
minItems: 1,
maxItems: 2,
elementConstraints: {
type: "object",
objectAttr: {
productId: { mandatory: true, type: "string" },
quantity: { mandatory: true, type: "number", min: 1 },
},
},
},
},
},
},
};A deep validation failure preserves the complete indexed path, for
example:
orders[1].items[2].quantityArray constraints work at nested levels too. A nested array can report paths such as:
orders[1].itemsNested arrays are supported and every array index is preserved:
matrix[1][1]
matrix[1][1][1]Transformations applied inside nested objects or array elements are preserved in validatedPayload, while the original input remains unchanged.
Transformations and Sanitization
perfectPayload() can transform a field before its remaining validation rules run. The transformed value is returned in validatedPayload, while the original input object is not mutated.
NOTE: if transform exist then all validation rules are applied to the transformed value, not the original input value.
Supported transformation rules:
| Rule | Purpose |
| ----------- | ----------------------------------------------------- |
| trim | Removes leading and trailing whitespace from strings. |
| lowercase | Converts strings to lowercase. |
| uppercase | Converts strings to uppercase. |
| transform | Runs a custom synchronous transformation function. |
The v2 validation flow is deterministic and does not depend on the order in which rule properties are written:
mandatory / presence
↓
trim
↓
lowercase
↓
uppercase
↓
transform
↓
allowNull
↓
type
↓
empty-container checks
↓
structural validation
↓
value constraints
↓
dependency
↓
customValidator
↓
validatedPayloadIf mandatory fails, transformations and all later rules for that field are skipped.
trim
const payload = {
name: " Kiran Poojary ",
};
const rules = {
name: {
type: "string",
trim: true,
},
};
const result = perfectPayload(payload, rules);
console.log(result.validatedPayload.name);
// "Kiran Poojary"
console.log(payload.name);
// " Kiran Poojary "trim applies only to string values. Non-string values are left unchanged.
lowercase
const rules = {
email: {
trim: true,
lowercase: true,
type: "email",
},
};For " [email protected] ", the validated value becomes "[email protected]".
uppercase
const rules = {
countryCode: {
type: "string",
uppercase: true,
},
};For "in", the validated value becomes "IN". lowercase: true and uppercase: true cannot be enabled together for the same field. Doing so throws a schema configuration error.
transform
Use transform when the built-in string transformations are not enough.
const rules = {
phone: {
type: "string",
transform: (value) => String(value).replace(/\s+/g, ""),
},
};For "98765 43210", the validated value becomes "9876543210".
A normal transformer receives two arguments:
transform: (value, payload) => {
return value;
};valueis the field value after the built-in transformations have run.payloadis the complete root payload/source object currently being validated.
For direct core usage, payload is the complete data object passed to perfectPayload() or perfectPayloadAsync(). For framework adapters, the transform receives the complete request source being validated:
body rule → request.body
headers rule → request.headers
params rule → request.params
query rule → request.queryThis root-payload contract is preserved inside nested objectAttr and elementConstraints validation.
Example cross-field transformation:
const payload = {
amount: 100,
multiplier: 2,
};
const rules = {
amount: {
transform: (value, payload) => value * payload.multiplier,
type: "number",
},
multiplier: {
type: "number",
},
};
const result = perfectPayload(payload, rules);
console.log(result.validatedPayload.amount);
// 200A custom transformer may also change the data type before validation:
const rules = {
quantity: {
transform: (value) => Number(value),
type: "number",
min: 1,
max: 100,
},
};For elementConstraints, the callback additionally receives the zero-based array index:
elementConstraints: {
transform: (value, index, payload) => {
return String(value).trim();
},
type: "string",
}The transformed value is validated by the normal validation rules and is also the value received by customValidator. Transformed nested/array values are preserved in validatedPayload. Missing optional fields are not transformed. An input value of null is not passed to transformation functions; null handling remains controlled by allowNull. Important: transform is synchronous. A non-function transformer, an async transformer, a transformer that returns a Promise, or a transformer that returns undefined is not supported and throws an error. Returning null, "", 0, or false is allowed; the transformed value is then processed by the remaining validation rules. Exceptions thrown inside the transformer propagate to the caller.
For example, returning undefined throws:
TypeError[ERR_UNDEFINED_TRANSFORM_RESULT]: transform must not return undefined for attribute "name"customValidator
Defines custom validation logic when the built-in rules are not enough.
The validator receives:
customValidator: (value, payload) => {
return true;
};valueis the field value after transformations have been applied.payloadis the current payload/object being validated.- Return
trueto pass. - Any value other than
truefails validation. - Exceptions thrown by the validator propagate to the caller.
For nested validation, payload means the current nested object rather than the root request body.
Synchronous custom validator
Use a synchronous validator with perfectPayload():
const rules = {
username: {
mandatory: true,
type: "string",
trim: true,
customValidator: (value) => {
return !value.toLowerCase().includes("admin");
},
customValidatorCode: "RESERVED_USERNAME",
customValidatorError: "Username cannot contain admin",
},
};
const result = perfectPayload({ username: " admin_kiran " }, rules);A failure returns:
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "username",
code: "RESERVED_USERNAME",
message: "Username cannot contain admin"
}
]
}The current payload/object can be used for cross-field validation:
const rules = {
limit: {
type: "number",
},
amount: {
type: "number",
customValidator: (value, payload) => {
return value <= payload.limit;
},
customValidatorCode: "LIMIT_EXCEEDED",
customValidatorError: "Amount cannot exceed limit",
},
};If customValidatorCode and customValidatorError are omitted, the
default error is:
{
path: "username",
code: "CUSTOM_VALIDATION_FAILED",
message: "Custom validation failed for attribute username"
}customValidator works recursively inside objectAttr and elementConstraints. Structured errors preserve the corresponding nested and array paths. When using perfectPayload(), customValidator must remain synchronous. A Promise-returning validator throws:
TypeError[ERR_ASYNC_CUSTOM_VALIDATOR]: customValidator must be synchronous for attribute "username"For asynchronous custom validation, use perfectPayloadAsync().
Asynchronous Validation
perfectPayloadAsync() supports both synchronous and asynchronous customValidator functions without changing the behavior of perfectPayload().
import { perfectPayloadAsync } from "perfect-payload";
const rules = {
username: {
mandatory: true,
type: "string",
trim: true,
customValidator: async (value) => {
const available = await checkUsernameAvailability(value);
return available;
},
customValidatorCode: "USERNAME_TAKEN",
customValidatorError: "Username is already taken",
},
};
const result = await perfectPayloadAsync(
{
username: " kiran ",
},
rules,
);On success, transformations are preserved:
{
statusCode: 200,
valid: true,
validatedPayload: {
username: "kiran"
}
}On asynchronous validation failure:
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "username",
code: "USERNAME_TAKEN",
message: "Username is already taken"
}
]
}Async validator contract
For perfectPayloadAsync():
true → pass
false → validation failure
anything != true → validation failure
throw → exception propagates
rejected Promise → rejection propagatesA normal synchronous validator is also valid when using the asynchronous
API:
const rules = {
username: {
type: "string",
customValidator: (value) => value !== "admin",
},
};
const result = await perfectPayloadAsync(payload, rules);A configured customValidator must be a function. Otherwise an error is
thrown:
TypeError[ERR_INVALID_CUSTOM_VALIDATOR]: customValidator must be a function for attribute usernameValidation order
perfectPayloadAsync() uses two phases:
- Run presence checks, transformations, and the normal deterministic synchronous validation pipeline.
- If phase 1 succeeds, run custom validators with
await.
If any synchronous validation error exists, phase 2 is skipped and the synchronous validation result is returned immediately. This means an async customValidator is pruned when an earlier rule such as type, regex, or another synchronous constraint has already failed.
Nested async validation
Async custom validators work recursively inside objectAttr:
const rules = {
profile: {
type: "object",
objectAttr: {
username: {
type: "string",
trim: true,
customValidator: async (value) => {
return await isUsernameAvailable(value);
},
customValidatorCode: "USERNAME_TAKEN",
customValidatorError: "Username is already taken",
},
},
},
};A failure produces the complete path:
profile.usernameThey also work inside elementConstraints:
const rules = {
usernames: {
type: "array",
elementConstraints: {
type: "string",
trim: true,
customValidator: async (value) => {
return await isUsernameAvailable(value);
},
customValidatorCode: "USERNAME_TAKEN",
customValidatorError: "Username is already taken",
},
},
};For an invalid second element:
usernames[1]Deep combinations of objects and arrays preserve every level of the
path:
products[1].seller.username
profile.teams[1].members[1].usernameDefault async custom-validation messages also use the final indexed
path:
{
path: "users[1].username",
code: "CUSTOM_VALIDATION_FAILED",
message: "Custom validation failed for attribute users[1].username"
}Transform remains synchronous
perfectPayloadAsync() makes custom validation asynchronous; it does not make transform asynchronous.
transform must still be synchronous:
transform: (value, payload) => {
return value;
};An async transformer or a transformer that returns a Promise is not supported.
Error Codes
perfectPayload() currently exposes the following machine-readable
validation error codes:
REQUIRED
NULL_NOT_ALLOWED
EMPTY_OBJECT_NOT_ALLOWED
EMPTY_ARRAY_NOT_ALLOWED
MIN_ITEMS
MAX_ITEMS
INVALID_ARRAY_ELEMENT
REGEX_MISMATCH
INVALID_TYPE
INVALID_EMAIL
INVALID_URL
INVALID_ENUM
INVALID_UUID
INVALID_UUID_V1
INVALID_UUID_V3
INVALID_UUID_V4
INVALID_UUID_V5
INVALID_OBJECT_ID
MIN_LENGTH
MAX_LENGTH
DECIMAL_NOT_ALLOWED
MIN_VALUE
MAX_VALUE
OUT_OF_RANGE
CUSTOM_VALIDATION_FAILED
UNKNOWN_FIELDThese codes are designed for programmatic handling while message remains suitable for human-readable API responses.
For example:
const result = perfectPayload(payload, validationRules);
if (!result.valid) {
const emailError = result.errors.find(
(error) => error.code === "INVALID_EMAIL",
);
if (emailError) {
// Handle invalid email
}
}Custom Error Messages
Every validation rule can use its corresponding custom error message. Custom messages replace the default human-readable message while keeping the same structured error format:
{
path: "email",
code: "INVALID_EMAIL",
message: "Email address is invalid"
}Example:
const rules = {
email: {
mandatory: true,
type: "email",
mandatoryError: "Email is required",
typeError: "Email address is invalid",
},
};If email is missing:
{
path: "email",
code: "REQUIRED",
message: "Email is required"
}If email is present but invalid:
{
path: "email",
code: "INVALID_EMAIL",
message: "Email address is invalid"
}Supported Custom Error Properties
| Validation Rule | Custom Error Property |
| -------------------- | ------------------------- |
| mandatory | mandatoryError |
| allowNull | allowNullError |
| allowEmptyObject | emptyObjectError |
| allowEmptyArray | emptyArrayError |
| elementConstraints | elementConstraintsError |
| regex | regexError |
| type | typeError |
| minLength | minLengthError |
| maxLength | maxLengthError |
| preventDecimal | preventDecimalError |
| min | minError |
| max | maxError |
| range | rangeError |
Example with Multiple Custom Errors
const payload = {
username: "ab",
age: 15,
score: 120,
};
const rules = {
username: {
mandatory: true,
type: "string",
minLength: 3,
mandatoryError: "Username is required",
typeError: "Username must be a string",
minLengthError: "Username must contain at least 3 characters",
},
age: {
type: "number",
min: 18,
minError: "Age must be at least 18",
},
score: {
type: "number",
range: "0-100",
rangeError: "Score must be between 0 and 100",
},
};
const result = perfectPayload(payload, rules);Example result:
{
statusCode: 400,
valid: false,
message:
"One or more attribute values are invalid",
errors: [
{
path: "username",
code: "MIN_LENGTH",
message:
"Username must contain at least 3 characters"
},
{
path: "age",
code: "MIN_VALUE",
message:
"Age must be at least 18"
},
{
path: "score",
code: "OUT_OF_RANGE",
message:
"Score must be between 0 and 100"
}
]
}Custom Messages and Error Codes
Custom messages only replace the message. They do not change the validation error code.
For example:
const rules = {
age: {
type: "number",
min: 18,
minError: "You must be 18 or older",
},
};Still returns:
{
path: "age",
code: "MIN_VALUE",
message: "You must be 18 or older"
}This makes it possible to:
- show custom messages to API consumers
- use stable error codes in application logic
- change user-facing wording without changing programmatic error
handling
Custom Response Objects
Custom valid and invalid response objects are configured inside the optional third options argument.
perfectPayload(data, validationRules, options?)
await perfectPayloadAsync(data, validationRules, options?)This keeps API-level configuration in one place and avoids positional undefined arguments.
Custom Valid Response
const result = perfectPayload(payload, validationRules, {
validPayloadResponse: {
statusCode: 201,
valid: true,
message: "Payload validated successfully",
},
});When validation succeeds, validatedPayload is automatically added:
{
statusCode: 201,
valid: true,
message: "Payload validated successfully",
validatedPayload: {
name: "Kiran",
email: "[email protected]",
age: 29
}
}Custom Invalid Response
const result = perfectPayload(payload, validationRules, {
inValidPayloadResponse: {
statusCode: 422,
valid: false,
message: "Payload validation failed",
},
});When validation fails, errors is automatically added:
{
statusCode: 422,
valid: false,
message: "Payload validation failed",
errors: [
{
path: "email",
code: "INVALID_EMAIL",
message: "Invalid email format for attribute email"
}
]
}Custom Valid and Invalid Responses Together
const result = perfectPayload(payload, validationRules, {
validPayloadResponse: {
statusCode: 201,
valid: true,
message: "CUSTOM_VALID_RESPONSE",
},
inValidPayloadResponse: {
statusCode: 422,
valid: false,
message: "CUSTOM_INVALID_RESPONSE",
},
});You can combine response customization with other API options:
const result = perfectPayload(payload, validationRules, {
unknownFields: "reject",
validPayloadResponse: {
statusCode: 201,
valid: true,
},
inValidPayloadResponse: {
statusCode: 422,
valid: false,
message: "Payload validation failed",
},
});The response object you provide is preserved while perfectPayload() automatically adds validatedPayload for successful validation or errors for failed validation. The same response options are supported by perfectPayloadAsync().
Migrating to the v2 API
The v2 perfectPayload() and perfectPayloadAsync() APIs use one optional third argument for configuration:
perfectPayload(data, validationRules, options?)
perfectPayloadAsync(data, validationRules, options?)Custom response objects now belong inside options.
Use:
perfectPayload(payload, rules, {
validPayloadResponse: customValidResponse,
inValidPayloadResponse: customInvalidResponse,
});instead of passing custom response objects as separate positional arguments. This also makes it possible to combine response customization with unknownFields without placeholder arguments:
perfectPayload(payload, rules, {
unknownFields: "reject",
inValidPayloadResponse: {
statusCode: 422,
valid: false,
message: "Payload validation failed",
},
});perfectPayloadV1() is unchanged and retains its legacy signature during its deprecation period.
Default Responses
If no custom response objects are provided, the default valid response is:
{
statusCode: 200,
valid: true,
validatedPayload: {
// validated fields
}
}The default invalid response is:
{
statusCode: 400,
valid: false,
message: "One or more attribute values are invalid",
errors: [
{
path: "field",
code: "ERROR_CODE",
message: "Validation error message"
}
]
}Nested Objects and Array Field Paths
perfectPayload() returns the exact location of a validation failure through the path property. This makes validation errors easier to map to API fields, forms, logs, and frontend components.
Top-Level Field
For a payload such as:
const payload = {
email: "invalid-email",
};An error can be returned as:
{
path: "email",
code: "INVALID_EMAIL",
message: "Invalid email format for attribute email"
}Nested Object
Use objectAttr to validate properties inside an object.
const payload = {
address: {
city: "Bengaluru",
location: {
latitude: "12.9716",
longitude: 77.5946,
},
},
};
const rules = {
address: {
type: "object",
objectAttr: {
city: {
type: "string",
},
location: {
type: "object",
objectAttr: {
latitude: {
type: "number",
},
longitude: {
type: "number",
},
},
},
},
},
};
const result = perfectPayload(payload, rules);Because latitude is a string instead of a number, the error contains its complete nested path:
{
path: "address.location.latitude",
code: "INVALID_TYPE",
message:
"Invalid type for attribute address.location.latitude, required number value"
}Nested paths use dot notation:
address.city
address.location.latitude
address.location.longitudeArray Elements
When elementConstraints validation fails, the array index is included in the error path.
const payload = {
marks: [50, 75, 150],
};
const rules = {
marks: {
type: "array",
elementConstraints: {
type: "number",
range: "0-100",
},
},
};
const result = perfectPayload(payload, rules);The invalid third element is reported as:
{
path: "marks[2]",
code: "OUT_OF_RANGE",
message:
"Attribute marks[2] should have a value between 0 and 100"
}Array paths use zero-based indexes:
marks[0]
marks[1]
marks[2]Array-level constraints such as minItems and maxItems report the path of the array itself. For nested arrays, the complete parent path is retained, for example orders[1].items.
Nested Fields Inside Arrays
Paths can also identify fields inside array elements.
For example:
products[0].quantity
products[1].quantity
products[2].priceThis provides enough information for consumers to identify the exact field that caused the validation error.
Why Structured Paths Are Useful
Instead of parsing an error message to determine which field failed, applications can directly use:
error.path;For example:
const result = perfectPayload(payload, validationRules);
if (!result.valid) {
result.errors.forEach((error) => {
console.log(error.path, error.code, error.message);
});
}A frontend can also map validation errors by path:
const fieldErrors = {};
result.errors.forEach((error) => {
fieldErrors[error.path] = error.message;
});Result:
{
"email": "Invalid email format for attribute email",
"address.location.latitude": "Invalid type for attribute address.location.latitude, required number value",
"marks[2]": "Attribute marks[2] should have a value between 0 and 100"
}Examples and Usage
Sample Validation Rule
sample-1
{
firstName: {
mandatory: true,
allowNull: false,
type: "string",
minLength: 3,
minLengthError: "First name must have minimum 3 characters."
},
lastName: {
mandatory: false,
allowNull: true,
type: "string",
},
email: {
mandatory: true,
allowNull: false,
type: "email",
},
phone: {
mandatory: true,
allowNull: false,
type: "string",
},
age: {
mandatory: false,
type: "number",
min: 1,
max: 120,
},
};sample-2
{
id: {
mandatory: true,
allowNull: true,
type: "uuidv4",
},
batchId: {
mandatory: true,
allowNull: true,
type: "objectId",
},
firstName: {
mandatory: true,
type: "string",
minLength: 3,
},
lastName: {
mandatory: false,
allowNull: true,
type: "string",
},
age: {
type: "number",
min: 0.1,
max: 120,
},
isAdult: {
type: "boolean",
},
totalWins: {
type: "number",
min: 0,
preventDecimal: true,
},
email: {
regex: /[^2]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/,
},
githubLink: {
type: "url",
},
accountStatus: {
type: "enum",
enumValues: ["Active", "Inactive", 200],
},
marks: {
range: "0-100",
},
allMarks: {
type: "array",
allowEmptyArray: false,
elementConstraints: {
type: "number",
allowNull: false,
range: "0-100",
},
},
totalScore: {
type: "number",
dependency: {
result: {
setDependencyRule: (totalScore, result) => {
return { mandatory: true, allowNull: false, type: "string" };
},
},
},
},
result: {
type: "string",
dependency: {
totalScore: {
setDependencyRule: (result, totalScore) => {
return { mandatory: true, allowNull: false, type: "number" };
},
},
},
},
minSalary: {
mandatory: true,
min: 1,
type: "number",
dependency: {
maxSalary: {
setDependencyRule: (minSalary, maxSalary) => {
return {
mandatory: true,
min: minSalary + 1,
minError: "maxSalary must be more than minSalary",
};
},
},
},
},
maxSalary: {
dependency: {
minSalary: {
setDependencyRule: (maxSalary, minSalary) => {
return {
mandatory: true,
max: maxSalary - 1,
maxError: "minSalary must be less than maxSalary",
};
},
},
},
},
address: {
mandatory: true,
type: "object",
allowEmptyObject: false,
objectAttr: {
country: { mandatory: true, type: "string" },
state: {
mandatory: true,
type: "string",
},
city: {},
zip: {
mandatory: true,
type: "string",
},
position: {
mandatory: true,
type: "object",
allowEmptyObject: false,
objectAttr: {
lattitude: { mandatory: true, type: "number" },
longitude: {
mandatory: true,
type: "number",
},
},
},
},
},
}Usage
Creating a route with payload validation middleware
// validatePayload is the middleware that invokes perfectPayload()
router.post(
"/payload-validation",
validatePayload({ rule: <your validation rule json object> }),
(req, res) => res.send("OK")
);ES Modules middleware example
import { perfectPayload } from "perfect-payload";
export const validatePayload = ({ rule }) => {
return (req, res, next) => {
try {
const { statusCode, ...response } = perfectPayload(req?.body, rule);
if (+statusCode >= 200 && +statusCode <= 299) {
req.validatedBody = response?.validatedPayload;
next();
} else res.status(statusCode).json(response);
} catch (error) {
console.error("Error validating payload", error);
res.status(500).json({ error: "Internal Server Error" });
}
};
};Async ES Modules middleware example
When your schema contains an asynchronous customValidator, the middleware itself must be async and perfectPayloadAsync() must be awaited:
import { perfectPayloadAsync } from "perfect-payload";
export const validatePayloadAsync = ({ rule }) => {
return async (req, res, next) => {
try {
const { statusCode, ...response } = await perfectPayloadAsync(
req?.body,
rule,
);
if (+statusCode >= 200 && +statusCode <= 299) {
req.validatedBody = response?.validatedPayload;
next();
} else {
res.status(statusCode).json(