perfect-payload 2.0.0-beta.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,29 +1,21 @@
1
1
  # perfect-payload
2
2
 
3
- A lightweight JavaScript library for validating, transforming, and
4
- sanitizing API and JSON payloads.
5
-
6
- `perfect-payload` provides structured validation errors, exact nested
7
- field paths, synchronous and asynchronous custom validation,
8
- transformations, nested object/array validation, unknown-field handling,
9
- and ready-to-use Express and Fastify integrations.
10
-
11
- The package is designed to stay simple and lightweight, with no Express
12
- or Fastify runtime dependency.
3
+ 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.
13
4
 
14
5
  ## Highlights
15
6
 
16
7
  - Lightweight, rule-based payload validation
8
+ - TypeScript-first package with built-in declarations — no separate `@types/perfect-payload`
17
9
  - Structured errors with stable machine-readable error codes
18
- - Exact nested paths such as `profile.email` and
19
- `products[1].quantity`
10
+ - Exact nested paths such as `profile.email` and `products[1].quantity`
20
11
  - Recursive `objectAttr` and `elementConstraints` validation
21
12
  - Array constraints with `minItems` and `maxItems`
22
13
  - Built-in `trim`, `lowercase`, and `uppercase` transformations
23
14
  - Custom synchronous `transform(value, payload)`
15
+ - Array-element transforms with `transform(value, index, payload)`
16
+ - Deterministic validation precedence independent of rule-property order
24
17
  - Synchronous custom validators with `perfectPayload()`
25
- - Synchronous or asynchronous custom validators with
26
- `perfectPayloadAsync()`
18
+ - Synchronous or asynchronous custom validators with `perfectPayloadAsync()`
27
19
  - Configurable unknown-field handling: `strip`, `allow`, or `reject`
28
20
  - Optional simplified errors with `prettyErrors`
29
21
  - Express middleware integration
@@ -33,7 +25,7 @@ or Fastify runtime dependency.
33
25
  - Framework-aware error paths such as `body.email` and `params.userId`
34
26
  - Transformed values returned through `validatedPayload`
35
27
  - Original input payload/request data is not mutated
36
- - No Express or Fastify runtime dependency
28
+ - Zero Express or Fastify runtime dependency
37
29
  - Legacy `perfectPayloadV1()` retained during the migration period
38
30
 
39
31
  ## Quick Links
@@ -41,38 +33,49 @@ or Fastify runtime dependency.
41
33
  - [Installation](#installation)
42
34
  - [Basic Usage](#basic-usage)
43
35
  - [Public API](#public-api)
36
+ - [TypeScript](#typescript)
44
37
  - [Options](#options)
45
- - [Pretty Errors](#pretty-errors)
38
+ - [Pretty Errors](#prettyerrors)
46
39
  - [Express Integration](#express-integration)
47
40
  - [Fastify Integration](#fastify-integration)
48
41
  - [Framework Request Validation](#framework-request-validation)
49
42
  - [Unknown Field Handling](#unknown-field-handling)
50
- - [Synchronous vs Asynchronous
51
- Validation](#synchronous-vs-asynchronous-validation)
43
+ - [Synchronous vs Asynchronous Validation](#synchronous-vs-asynchronous-validation)
52
44
  - [Validation Rules](#validation-rules)
53
- - [Array Size and Nested
54
- Validation](#array-size-and-nested-validation)
55
- - [Transformations and
56
- Sanitization](#transformations-and-sanitization)
45
+ - [Validation Precedence](#validation-precedence)
46
+ - [Array Size and Nested Validation](#array-size-and-nested-validation)
47
+ - [Transformations and Sanitization](#transformations-and-sanitization)
57
48
  - [Custom Validators](#customvalidator)
58
49
  - [Asynchronous Validation](#asynchronous-validation)
59
50
  - [Error Codes](#error-codes)
60
51
  - [Custom Error Messages](#custom-error-messages)
61
- - [Nested Objects and Array Field
62
- Paths](#nested-objects-and-array-field-paths)
52
+ - [Nested Objects and Array Field Paths](#nested-objects-and-array-field-paths)
63
53
  - [Examples and Usage](#examples-and-usage)
64
54
  - [Legacy API](#legacy-api)
65
55
 
66
- ## What's New in v1.8.0
56
+ ## What's New in v2.0.0
67
57
 
68
- v1.8.0 adds first-class framework integrations and simpler error output
69
- while keeping the core validation API framework-independent.
58
+ v2.0.0 establishes the stable TypeScript-first architecture for `perfect-payload` while keeping the package lightweight and framework-independent.
70
59
 
71
- ## Express Integration
60
+ Major v2 additions and hardening include:
61
+
62
+ - Built-in TypeScript declarations generated from the source — no separate `@types/perfect-payload` package.
63
+ - Public validation types such as `ValidationRules` and `ValidationOptions`.
64
+ - Structured validation errors with stable `path`, `code`, and `message` fields.
65
+ - `prettyErrors` for consumers that prefer a simple string array.
66
+ - Recursive unknown-field handling with `strip`, `allow`, and `reject`.
67
+ - Exact nested and indexed paths across `objectAttr` and `elementConstraints`.
68
+ - Synchronous transforms with a complete root-payload context.
69
+ - Array-element transforms with `(value, index, payload)`.
70
+ - Deterministic validation precedence that does not depend on the order in which rule properties are written.
71
+ - `perfectPayloadAsync()` with async `customValidator` support and sync-error pruning.
72
+ - Lightweight Express and Fastify adapters for `headers`, `params`, `query`, and `body`.
73
+ - Framework-aware generated error paths/messages while preserving custom error messages exactly as configured.
74
+ - Legacy `perfectPayloadV1()` retained during the migration period.
72
75
 
73
- `perfect-payload` provides a lightweight Express adapter so request validation can be added directly as middleware.
76
+ ## Express Integration
74
77
 
75
- Express is **not** installed as a dependency of `perfect-payload`.
78
+ `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`.
76
79
 
77
80
  ### Import
78
81
 
@@ -87,11 +90,8 @@ Use `validatePayload()` when your validation rules are synchronous.
87
90
  ```js
88
91
  import express from "express";
89
92
  import { validatePayload } from "perfect-payload/express";
90
-
91
93
  const app = express();
92
-
93
94
  app.use(express.json());
94
-
95
95
  const userRules = {
96
96
  email: {
97
97
  mandatory: true,
@@ -105,7 +105,6 @@ const userRules = {
105
105
  min: 18,
106
106
  },
107
107
  };
108
-
109
108
  app.post(
110
109
  "/users",
111
110
  validatePayload({
@@ -115,7 +114,6 @@ app.post(
115
114
  }),
116
115
  (req, res) => {
117
116
  const user = req.validatedPayload.body;
118
-
119
117
  res.json({
120
118
  message: "User created",
121
119
  user,
@@ -158,20 +156,17 @@ app.post(
158
156
  type: "string",
159
157
  },
160
158
  },
161
-
162
159
  params: {
163
160
  userId: {
164
161
  mandatory: true,
165
162
  type: "string",
166
163
  },
167
164
  },
168
-
169
165
  query: {
170
166
  notify: {
171
167
  type: "boolean",
172
168
  },
173
169
  },
174
-
175
170
  body: {
176
171
  email: {
177
172
  mandatory: true,
@@ -184,7 +179,6 @@ app.post(
184
179
  }),
185
180
  (req, res) => {
186
181
  const { headers, params, query, body } = req.validatedPayload;
187
-
188
182
  res.json({
189
183
  headers,
190
184
  params,
@@ -212,13 +206,13 @@ Structured error paths include the request source:
212
206
  {
213
207
  path: "body.email",
214
208
  code: "INVALID_EMAIL",
215
- message: "Invalid email format for attribute email"
209
+ message: "Invalid email format for attribute body.email"
216
210
  }
217
211
  ]
218
212
  }
219
213
  ```
220
214
 
221
- The request source is added to the structured `path`, while the validation message itself is preserved.
215
+ 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.
222
216
 
223
217
  ### Adapter Options
224
218
 
@@ -247,7 +241,6 @@ Use `validatePayloadAsync()` when the schema contains asynchronous `customValida
247
241
 
248
242
  ```js
249
243
  import { validatePayloadAsync } from "perfect-payload/express";
250
-
251
244
  app.post(
252
245
  "/users",
253
246
  validatePayloadAsync({
@@ -257,11 +250,9 @@ app.post(
257
250
  mandatory: true,
258
251
  type: "string",
259
252
  trim: true,
260
-
261
253
  customValidator: async (value) => {
262
254
  return await isUsernameAvailable(value);
263
255
  },
264
-
265
256
  customValidatorCode: "USERNAME_TAKEN",
266
257
  customValidatorError: "Username is already taken",
267
258
  },
@@ -283,9 +274,7 @@ Validation failures are handled by the middleware automatically. Unexpected erro
283
274
 
284
275
  ## Fastify Integration
285
276
 
286
- `perfect-payload` provides a lightweight Fastify adapter that can be used directly as a route hook.
287
-
288
- Fastify is **not** installed as a dependency of `perfect-payload`.
277
+ `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`.
289
278
 
290
279
  ### Import
291
280
 
@@ -300,9 +289,7 @@ Use the adapter as a Fastify `preValidation` hook:
300
289
  ```js
301
290
  import Fastify from "fastify";
302
291
  import { validatePayload } from "perfect-payload/fastify";
303
-
304
292
  const fastify = Fastify();
305
-
306
293
  const userRules = {
307
294
  email: {
308
295
  mandatory: true,
@@ -316,7 +303,6 @@ const userRules = {
316
303
  min: 18,
317
304
  },
318
305
  };
319
-
320
306
  fastify.post(
321
307
  "/users",
322
308
  {
@@ -328,7 +314,6 @@ fastify.post(
328
314
  },
329
315
  async (request, reply) => {
330
316
  const user = request.validatedPayload.body;
331
-
332
317
  return {
333
318
  message: "User created",
334
319
  user,
@@ -372,20 +357,17 @@ fastify.post(
372
357
  type: "string",
373
358
  },
374
359
  },
375
-
376
360
  params: {
377
361
  userId: {
378
362
  mandatory: true,
379
363
  type: "string",
380
364
  },
381
365
  },
382
-
383
366
  query: {
384
367
  notify: {
385
368
  type: "boolean",
386
369
  },
387
370
  },
388
-
389
371
  body: {
390
372
  email: {
391
373
  mandatory: true,
@@ -399,7 +381,6 @@ fastify.post(
399
381
  },
400
382
  async (request, reply) => {
401
383
  const { headers, params, query, body } = request.validatedPayload;
402
-
403
384
  return {
404
385
  headers,
405
386
  params,
@@ -427,13 +408,13 @@ Structured error paths include the request source:
427
408
  {
428
409
  path: "body.email",
429
410
  code: "INVALID_EMAIL",
430
- message: "Invalid email format for attribute email"
411
+ message: "Invalid email format for attribute body.email"
431
412
  }
432
413
  ]
433
414
  }
434
415
  ```
435
416
 
436
- The request source is added to the structured `path`, while the validation message itself is preserved.
417
+ 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.
437
418
 
438
419
  ### Adapter Options
439
420
 
@@ -462,7 +443,6 @@ Use `validatePayloadAsync()` when the schema contains asynchronous `customValida
462
443
 
463
444
  ```js
464
445
  import { validatePayloadAsync } from "perfect-payload/fastify";
465
-
466
446
  fastify.post(
467
447
  "/users",
468
448
  {
@@ -473,11 +453,9 @@ fastify.post(
473
453
  mandatory: true,
474
454
  type: "string",
475
455
  trim: true,
476
-
477
456
  customValidator: async (value) => {
478
457
  return await isUsernameAvailable(value);
479
458
  },
480
-
481
459
  customValidatorCode: "USERNAME_TAKEN",
482
460
  customValidatorError: "Username is already taken",
483
461
  },
@@ -500,24 +478,48 @@ Validation failures are handled by the hook automatically. Unexpected errors fro
500
478
 
501
479
  ### Framework-aware error paths
502
480
 
503
- Structured validation errors include the request source in their path:
481
+ Structured framework validation errors include the request source:
504
482
 
505
483
  ```js
506
484
  {
507
485
  path: "body.email",
508
486
  code: "INVALID_EMAIL",
509
- message: "Invalid email format for attribute email"
487
+ message: "Invalid email format for attribute body.email"
510
488
  }
511
489
  ```
512
490
 
513
- The `path` identifies the exact request source and field. Custom and
514
- default validation messages are preserved rather than rewritten by the
515
- framework adapter.
491
+ Generated default errors use source-aware paths such as:
492
+
493
+ ```text
494
+ headers.x-request-id
495
+ params.userId
496
+ query.page
497
+ body.email
498
+ ```
499
+
500
+ A generated pretty error can therefore look like:
501
+
502
+ ```text
503
+ Custom validation failed for attribute headers.x-test-id
504
+ ```
505
+
506
+ Custom developer-defined messages are preserved exactly. For example:
507
+
508
+ ```js
509
+ customValidatorError: "Invalid x-request-id in headers";
510
+ ```
511
+
512
+ returns:
513
+
514
+ ```text
515
+ Invalid x-request-id in headers
516
+ ```
517
+
518
+ The framework adapter does not prepend `headers.`, `params.`, `query.`, or `body.` to a custom message.
516
519
 
517
520
  ### `prettyErrors`
518
521
 
519
- v1.8.0 introduces the `prettyErrors` option. Structured errors remain
520
- the default.
522
+ `prettyErrors` provides a simplified string-error mode. Structured errors remain the default.
521
523
 
522
524
  ```js
523
525
  const result = perfectPayload(payload, rules, {
@@ -525,26 +527,16 @@ const result = perfectPayload(payload, rules, {
525
527
  });
526
528
  ```
527
529
 
528
- With `prettyErrors: true`, the `errors` array contains human-readable
529
- message strings instead of structured error objects.
530
-
531
- `prettyErrors` is also supported by `perfectPayloadAsync()` and the
532
- Express/Fastify integrations.
530
+ 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.
533
531
 
534
532
  ### Error privacy
535
533
 
536
- Default validation messages do not include submitted payload values.
537
-
538
- Schema constraints such as allowed enum values, minimums, maximums, and
539
- ranges may still appear in validation messages. Custom error messages
540
- are controlled by the application and are returned as configured.
534
+ 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.
541
535
 
542
536
  ### Lightweight framework integrations
543
537
 
544
- Express and Fastify are **not installed as dependencies of
545
- `perfect-payload`**.
538
+ Express and Fastify are **not installed as dependencies of `perfect-payload`**. The framework integrations are thin adapters around the same validation
546
539
 
547
- The framework integrations are thin adapters around the same validation
548
540
  engine used by:
549
541
 
550
542
  ```js
@@ -552,13 +544,11 @@ perfectPayload();
552
544
  perfectPayloadAsync();
553
545
  ```
554
546
 
555
- This keeps the package lightweight while allowing framework users to
556
- integrate validation without writing their own middleware or hooks.
547
+ This keeps the package lightweight while allowing framework users to integrate validation without writing their own middleware or hooks.
557
548
 
558
549
  ## Installation
559
550
 
560
551
  ```bash
561
-
562
552
  npm install perfect-payload
563
553
  ```
564
554
 
@@ -568,117 +558,75 @@ Use `perfectPayload()` for all new implementations.
568
558
 
569
559
  ```js
570
560
  import { perfectPayload } from "perfect-payload";
571
-
572
561
  const payload = {
573
562
  name: "Kiran",
574
-
575
563
  email: "kiran@example.com",
576
-
577
564
  age: 29,
578
565
  };
579
-
580
566
  const validationRules = {
581
567
  name: {
582
568
  mandatory: true,
583
-
584
569
  type: "string",
585
570
  },
586
-
587
571
  email: {
588
572
  mandatory: true,
589
-
590
573
  type: "email",
591
574
  },
592
-
593
575
  age: {
594
576
  mandatory: true,
595
-
596
577
  type: "number",
597
-
598
578
  min: 18,
599
579
  },
600
580
  };
601
-
602
581
  const result = perfectPayload(payload, validationRules);
603
-
604
582
  console.log(result);
605
583
  ```
606
584
 
607
585
  ### Valid Response
608
586
 
609
587
  ```js
610
-
611
588
  {
612
-
613
589
  statusCode: 200,
614
-
615
590
  valid: true,
616
-
617
591
  validatedPayload: {
618
-
619
592
  name: "Kiran",
620
-
621
593
  email: "kiran@example.com",
622
-
623
594
  age: 29
624
-
625
595
  }
626
-
627
596
  }
628
597
  ```
629
598
 
630
- 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"`.
631
-
632
- The original input payload is not mutated. (such as validatedBody, sanitisedData or parsedBody).
599
+ 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).
633
600
 
634
601
  ### Invalid Response
635
602
 
636
603
  ```js
637
-
638
604
  {
639
-
640
605
  statusCode: 400,
641
-
642
606
  valid: false,
643
-
644
607
  message: "One or more attribute values are invalid",
645
-
646
608
  errors: [
647
-
648
609
  {
649
-
650
610
  path: "email",
651
-
652
611
  code: "INVALID_EMAIL",
653
-
654
612
  message: "Invalid email format for attribute email"
655
-
656
613
  }
657
-
658
614
  ]
659
-
660
615
  }
661
616
  ```
662
617
 
663
618
  Each error returned by `perfectPayload()` contains:
664
619
 
665
620
  ```js
666
-
667
621
  {
668
-
669
622
  path: "field.path",
670
-
671
623
  code: "ERROR_CODE",
672
-
673
624
  message: "Human readable validation message"
674
-
675
625
  }
676
626
  ```
677
627
 
678
628
  - `path` identifies the exact field that failed validation.
679
-
680
629
  - `code` provides a stable machine-readable validation error code.
681
-
682
630
  - `message` provides a human-readable description of the validation
683
631
 
684
632
  failure.
@@ -687,12 +635,10 @@ failure.
687
635
 
688
636
  ## Public API
689
637
 
690
- For new implementations, both supported APIs use the same clean three-argument signature:
638
+ For v2 and all new implementations, both supported APIs use the same clean three-argument signature:
691
639
 
692
640
  ```js
693
-
694
641
  perfectPayload(data, validationRules, options?)
695
-
696
642
  await perfectPayloadAsync(data, validationRules, options?)
697
643
  ```
698
644
 
@@ -700,67 +646,99 @@ The arguments are:
700
646
 
701
647
  | Argument | Required | Description |
702
648
  | ----------------- | -------- | ----------------------------------------------------------------------------------- |
703
- | `data` | No | Payload/object to validate. Defaults to `{}`. |
704
- | `validationRules` | No | Validation schema. Defaults to `{}`. |
649
+ | `data` | No | Payload/object to validate. Defaults to`{}`. |
650
+ | `validationRules` | No | Validation schema. Defaults to`{}`. |
705
651
  | `options` | No | API-level configuration such as unknown-field handling and custom response objects. |
706
652
 
707
- The third argument is a single options object. You no longer need to pass separate positional arguments for custom valid and invalid responses.
708
-
709
- ### Options
710
-
711
- ```js
712
-
713
- {
653
+ 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`.
714
654
 
715
- unknownFields: "strip" | "allow" | "reject",
655
+ ### TypeScript
716
656
 
717
- validPayloadResponse: {
657
+ `perfect-payload` ships its TypeScript declarations with the package. No separate `@types/perfect-payload` installation is required.
718
658
 
719
- statusCode: 200,
659
+ ```ts
660
+ import { perfectPayload, perfectPayloadAsync } from "perfect-payload";
661
+ import type {
662
+ ValidationOptions,
663
+ ValidationRules,
664
+ AttributeValidationRules,
665
+ ValidationValueType,
666
+ TransformFunction,
667
+ ArrayElementTransformFunction,
668
+ CustomValidator,
669
+ DependencyRule,
670
+ DependencyRules,
671
+ } from "perfect-payload";
672
+ ```
720
673
 
721
- valid: true,
674
+ Example:
722
675
 
676
+ ```ts
677
+ const rules: ValidationRules = {
678
+ email: {
679
+ mandatory: true,
680
+ type: "email",
681
+ trim: true,
682
+ lowercase: true,
723
683
  },
684
+ };
685
+ const options: ValidationOptions = {
686
+ unknownFields: "reject",
687
+ prettyErrors: false,
688
+ };
689
+ const result = perfectPayload(
690
+ {
691
+ email: " KIRAN@EXAMPLE.COM ",
692
+ },
693
+ rules,
694
+ options,
695
+ );
696
+ if (result.valid === true) {
697
+ console.log(result.validatedPayload);
698
+ } else {
699
+ console.log(result.errors);
700
+ }
701
+ ```
724
702
 
725
- inValidPayloadResponse: {
726
-
727
- statusCode: 400,
728
-
729
- valid: false,
703
+ `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.
730
704
 
731
- message: "One or more attribute values are invalid",
705
+ ### Options
732
706
 
733
- },
707
+ The third argument uses the `ValidationOptions` type in TypeScript.
734
708
 
709
+ ```ts
710
+ interface ValidationOptions {
711
+ unknownFields?: "strip" | "allow" | "reject";
712
+ prettyErrors?: boolean;
713
+ validPayloadResponse?: {
714
+ statusCode: number;
715
+ valid: true;
716
+ [key: string]: unknown;
717
+ };
718
+ inValidPayloadResponse?: {
719
+ statusCode: number;
720
+ valid: false;
721
+ message: string;
722
+ [key: string]: unknown;
723
+ };
735
724
  }
736
725
  ```
737
726
 
738
727
  All properties are optional. The defaults are equivalent to:
739
728
 
740
729
  ```js
741
-
742
730
  {
743
-
744
731
  unknownFields: "strip",
745
-
732
+ prettyErrors: false,
746
733
  validPayloadResponse: {
747
-
748
734
  statusCode: 200,
749
-
750
735
  valid: true,
751
-
752
736
  },
753
-
754
737
  inValidPayloadResponse: {
755
-
756
738
  statusCode: 400,
757
-
758
739
  valid: false,
759
-
760
740
  message: "One or more attribute values are invalid",
761
-
762
741
  },
763
-
764
742
  }
765
743
  ```
766
744
 
@@ -769,20 +747,15 @@ Example:
769
747
  ```js
770
748
  const result = perfectPayload(payload, validationRules, {
771
749
  unknownFields: "reject",
772
-
750
+ prettyErrors: false,
773
751
  validPayloadResponse: {
774
752
  statusCode: 201,
775
-
776
753
  valid: true,
777
-
778
754
  message: "Payload accepted",
779
755
  },
780
-
781
756
  inValidPayloadResponse: {
782
757
  statusCode: 422,
783
-
784
758
  valid: false,
785
-
786
759
  message: "Payload validation failed",
787
760
  },
788
761
  });
@@ -793,12 +766,10 @@ The same options object is supported by `perfectPayloadAsync()`:
793
766
  ```js
794
767
  const result = await perfectPayloadAsync(payload, validationRules, {
795
768
  unknownFields: "reject",
796
-
769
+ prettyErrors: false,
797
770
  inValidPayloadResponse: {
798
771
  statusCode: 422,
799
-
800
772
  valid: false,
801
-
802
773
  message: "Payload validation failed",
803
774
  },
804
775
  });
@@ -810,46 +781,36 @@ const result = await perfectPayloadAsync(payload, validationRules, {
810
781
 
811
782
  Supported values:
812
783
 
813
- | Value | Behavior |
814
- | ---------- | -------------------------------------------------------------------------------------------------------- |
815
- | `"strip"` | Removes unknown fields from `validatedPayload`. This is the default and preserves the existing behavior. |
816
- | `"allow"` | Preserves unknown fields in `validatedPayload`. |
817
- | `"reject"` | Rejects unknown fields with structured `UNKNOWN_FIELD` validation errors. |
784
+ | Value | Behavior |
785
+ | ---------- | ------------------------------------------------------------------------------------------------------- |
786
+ | `"strip"` | Removes unknown fields from`validatedPayload`. This is the default and preserves the existing behavior. |
787
+ | `"allow"` | Preserves unknown fields in`validatedPayload`. |
788
+ | `"reject"` | Rejects unknown fields with structured`UNKNOWN_FIELD` validation errors. |
818
789
 
819
790
  ### `strip` — default
820
791
 
821
792
  ```js
822
793
  const payload = {
823
794
  name: "Kiran",
824
-
825
795
  role: "developer",
826
796
  };
827
-
828
797
  const rules = {
829
798
  name: {
830
799
  type: "string",
831
800
  },
832
801
  };
833
-
834
802
  const result = perfectPayload(payload, rules);
835
803
  ```
836
804
 
837
805
  Result:
838
806
 
839
807
  ```js
840
-
841
808
  {
842
-
843
809
  statusCode: 200,
844
-
845
810
  valid: true,
846
-
847
811
  validatedPayload: {
848
-
849
812
  name: "Kiran"
850
-
851
813
  }
852
-
853
814
  }
854
815
  ```
855
816
 
@@ -863,7 +824,7 @@ perfectPayload(payload, rules, {
863
824
  });
864
825
  ```
865
826
 
866
- ### `allow` --- preserve unknown fields
827
+ ### `allow` — preserve unknown fields
867
828
 
868
829
  ```js
869
830
  const result = perfectPayload(payload, rules, {
@@ -874,29 +835,19 @@ const result = perfectPayload(payload, rules, {
874
835
  Result:
875
836
 
876
837
  ```js
877
-
878
838
  {
879
-
880
839
  statusCode: 200,
881
-
882
840
  valid: true,
883
-
884
841
  validatedPayload: {
885
-
886
842
  name: "Kiran",
887
-
888
843
  role: "developer"
889
-
890
844
  }
891
-
892
845
  }
893
846
  ```
894
847
 
895
- Schema-defined fields are still validated normally. Unknown fields are
896
-
897
- simply preserved.
848
+ Schema-defined fields are still validated normally. Unknown fields are simply preserved.
898
849
 
899
- ### `reject` --- reject unknown fields
850
+ ### `reject` — reject unknown fields
900
851
 
901
852
  ```js
902
853
  const result = perfectPayload(payload, rules, {
@@ -907,29 +858,17 @@ const result = perfectPayload(payload, rules, {
907
858
  Result:
908
859
 
909
860
  ```js
910
-
911
861
  {
912
-
913
862
  statusCode: 400,
914
-
915
863
  valid: false,
916
-
917
864
  message: "One or more attribute values are invalid",
918
-
919
865
  errors: [
920
-
921
866
  {
922
-
923
867
  path: "role",
924
-
925
868
  code: "UNKNOWN_FIELD",
926
-
927
869
  message: "Unknown field role is not allowed"
928
-
929
870
  }
930
-
931
871
  ]
932
-
933
872
  }
934
873
  ```
935
874
 
@@ -943,15 +882,12 @@ Unknown-field handling is recursive for schemas using `objectAttr`.
943
882
  const payload = {
944
883
  profile: {
945
884
  city: "Bengaluru",
946
-
947
885
  role: "developer",
948
886
  },
949
887
  };
950
-
951
888
  const rules = {
952
889
  profile: {
953
890
  type: "object",
954
-
955
891
  objectAttr: {
956
892
  city: {
957
893
  type: "string",
@@ -959,7 +895,6 @@ const rules = {
959
895
  },
960
896
  },
961
897
  };
962
-
963
898
  const result = perfectPayload(payload, rules, {
964
899
  unknownFields: "reject",
965
900
  });
@@ -968,29 +903,17 @@ const result = perfectPayload(payload, rules, {
968
903
  Returns:
969
904
 
970
905
  ```js
971
-
972
906
  {
973
-
974
907
  statusCode: 400,
975
-
976
908
  valid: false,
977
-
978
909
  message: "One or more attribute values are invalid",
979
-
980
910
  errors: [
981
-
982
911
  {
983
-
984
912
  path: "profile.role",
985
-
986
913
  code: "UNKNOWN_FIELD",
987
-
988
914
  message: "Unknown field profile.role is not allowed"
989
-
990
915
  }
991
-
992
916
  ]
993
-
994
917
  }
995
918
  ```
996
919
 
@@ -1001,15 +924,10 @@ Returns:
1001
924
  the array index:
1002
925
 
1003
926
  ```js
1004
-
1005
927
  {
1006
-
1007
928
  path: "products[0].internalId",
1008
-
1009
929
  code: "UNKNOWN_FIELD",
1010
-
1011
930
  message: "Unknown field products[0].internalId is not allowed"
1012
-
1013
931
  }
1014
932
  ```
1015
933
 
@@ -1018,7 +936,6 @@ This continues through deeply nested combinations of objects and arrays,
1018
936
  for example:
1019
937
 
1020
938
  ```text
1021
-
1022
939
  profile.teams[0].members[0].role
1023
940
  ```
1024
941
 
@@ -1029,49 +946,27 @@ With `"reject"`, unknown-field errors can be returned together with normal valid
1029
946
  For example, an invalid email plus two unknown fields can produce:
1030
947
 
1031
948
  ```js
1032
-
1033
949
  {
1034
-
1035
950
  statusCode: 400,
1036
-
1037
951
  valid: false,
1038
-
1039
952
  message: "One or more attribute values are invalid",
1040
-
1041
953
  errors: [
1042
-
1043
954
  {
1044
-
1045
955
  path: "email",
1046
-
1047
956
  code: "INVALID_EMAIL",
1048
-
1049
957
  message: "Invalid email format for attribute email"
1050
-
1051
958
  },
1052
-
1053
959
  {
1054
-
1055
960
  path: "role",
1056
-
1057
961
  code: "UNKNOWN_FIELD",
1058
-
1059
962
  message: "Unknown field role is not allowed"
1060
-
1061
963
  },
1062
-
1063
964
  {
1064
-
1065
965
  path: "active",
1066
-
1067
966
  code: "UNKNOWN_FIELD",
1068
-
1069
967
  message: "Unknown field active is not allowed"
1070
-
1071
968
  }
1072
-
1073
969
  ]
1074
-
1075
970
  }
1076
971
  ```
1077
972
 
@@ -1098,18 +993,14 @@ Unknown-field handling considers only the payload object's own enumerable proper
1098
993
  Only these values are accepted:
1099
994
 
1100
995
  ```text
1101
-
1102
996
  strip
1103
-
1104
997
  allow
1105
-
1106
998
  reject
1107
999
  ```
1108
1000
 
1109
1001
  Any other value throws a configuration error:
1110
1002
 
1111
1003
  ```text
1112
-
1113
1004
  perfect-payload:- unknownFields must be one of strip, allow, reject
1114
1005
  ```
1115
1006
 
@@ -1121,7 +1012,6 @@ For normal synchronous validation, use `perfectPayload()`:
1121
1012
 
1122
1013
  ```js
1123
1014
  import { perfectPayload } from "perfect-payload";
1124
-
1125
1015
  const result = perfectPayload(payload, validationRules, options);
1126
1016
  ```
1127
1017
 
@@ -1129,50 +1019,29 @@ When any `customValidator` needs to perform asynchronous work, use `perfectPaylo
1129
1019
 
1130
1020
  ```js
1131
1021
  import { perfectPayloadAsync } from "perfect-payload";
1132
-
1133
- const result = await perfectPayloadAsync(
1134
- payload,
1135
- validationRules,
1136
-
1137
- options,
1138
- );
1022
+ const result = await perfectPayloadAsync(payload, validationRules, options);
1139
1023
  ```
1140
1024
 
1141
1025
  The public APIs are:
1142
1026
 
1143
1027
  ```text
1144
-
1145
1028
  perfectPayloadV1() legacy API; deprecated
1146
-
1147
1029
  perfectPayload(data, rules, options?) synchronous validation
1148
-
1149
1030
  perfectPayloadAsync(data, rules, options?) synchronous + asynchronous
1150
-
1151
1031
  customValidator
1152
1032
  ```
1153
1033
 
1154
- `perfectPayload()` remains synchronous and intentionally rejects a `customValidator` that returns a Promise. This preserves the existing synchronous API contract.
1155
-
1156
- `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.
1034
+ `perfectPayload()` 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.
1157
1035
 
1158
1036
  ```text
1159
-
1160
1037
  transformations
1161
-
1162
1038
  ↓
1163
-
1164
1039
  synchronous validation
1165
-
1166
1040
  ↓
1167
-
1168
1041
  sync errors? ── yes ──→ return validation errors
1169
-
1170
1042
  ↓ no
1171
-
1172
1043
  async customValidator
1173
-
1174
1044
  ↓
1175
-
1176
1045
  return result
1177
1046
  ```
1178
1047
 
@@ -1184,25 +1053,17 @@ return result
1184
1053
  import { perfectPayloadV1 } from "perfect-payload";
1185
1054
  ```
1186
1055
 
1187
- `perfectPayloadV1()` is deprecated and will no longer be supported
1188
-
1189
- after
1190
-
1191
- March 31, 2027.
1192
-
1193
- Existing applications can continue using it during the migration period,
1056
+ `perfectPayloadV1()` is deprecated and will no longer be supported after March 31, 2027. Existing applications can continue using it during the migration period,
1194
1057
 
1195
1058
  but all new implementations should use the current API:
1196
1059
 
1197
1060
  ```js
1198
-
1199
1061
  perfectPayload(data, validationRules, options?);
1200
1062
  ```
1201
1063
 
1202
1064
  For asynchronous custom validation:
1203
1065
 
1204
1066
  ```js
1205
-
1206
1067
  await perfectPayloadAsync(data, validationRules, options?);
1207
1068
  ```
1208
1069
 
@@ -1218,9 +1079,7 @@ while the new `perfectPayload()` API returns structured errors:
1218
1079
  errors: [
1219
1080
  {
1220
1081
  path: "email",
1221
-
1222
1082
  code: "INVALID_EMAIL",
1223
-
1224
1083
  message: "Invalid email format for attribute email",
1225
1084
  },
1226
1085
  ];
@@ -1234,9 +1093,7 @@ Note: If an inValidPayloadResponse is provided in the options, the system return
1234
1093
 
1235
1094
  ### `mandatory`
1236
1095
 
1237
- Marks a field as required. An empty string is also treated as missing.
1238
-
1239
- Default: `false`, the field is not required.
1096
+ Marks a field as required. An empty string is also treated as missing. Default: `false`, the field is not required.
1240
1097
 
1241
1098
  ```js
1242
1099
  const rules = {
@@ -1246,15 +1103,11 @@ const rules = {
1246
1103
  };
1247
1104
  ```
1248
1105
 
1249
- Error code: `REQUIRED`
1250
-
1251
- ---
1106
+ ## Error code: `REQUIRED`
1252
1107
 
1253
1108
  ### `allowNull`
1254
1109
 
1255
- Controls whether `null` values are accepted.
1256
-
1257
- Default: `true`, `null` values are allowed.
1110
+ Controls whether `null` values are accepted. Default: `true`, `null` values are allowed.
1258
1111
 
1259
1112
  Example:
1260
1113
 
@@ -1266,15 +1119,11 @@ const rules = {
1266
1119
  };
1267
1120
  ```
1268
1121
 
1269
- Error code: `NULL_NOT_ALLOWED`
1270
-
1271
- ---
1122
+ ## Error code: `NULL_NOT_ALLOWED`
1272
1123
 
1273
1124
  ### `allowEmptyObject`
1274
1125
 
1275
- Controls whether an empty object `{}` is accepted.
1276
-
1277
- Default: `true`, empty objects are allowed.
1126
+ Controls whether an empty object `{}` is accepted. Default: `true`, empty objects are allowed.
1278
1127
 
1279
1128
  Example:
1280
1129
 
@@ -1282,21 +1131,16 @@ Example:
1282
1131
  const rules = {
1283
1132
  address: {
1284
1133
  type: "object",
1285
-
1286
1134
  allowEmptyObject: false,
1287
1135
  },
1288
1136
  };
1289
1137
  ```
1290
1138
 
1291
- Error code: `EMPTY_OBJECT_NOT_ALLOWED`
1292
-
1293
- ---
1139
+ ## Error code: `EMPTY_OBJECT_NOT_ALLOWED`
1294
1140
 
1295
1141
  ### `allowEmptyArray`
1296
1142
 
1297
- Controls whether an empty array `[]` is accepted.
1298
-
1299
- Default: `true`, empty arrays are allowed.
1143
+ Controls whether an empty array `[]` is accepted. Default: `true`, empty arrays are allowed.
1300
1144
 
1301
1145
  Example:
1302
1146
 
@@ -1304,27 +1148,21 @@ Example:
1304
1148
  const rules = {
1305
1149
  products: {
1306
1150
  type: "array",
1307
-
1308
1151
  allowEmptyArray: false,
1309
1152
  },
1310
1153
  };
1311
1154
  ```
1312
1155
 
1313
- Error code: `EMPTY_ARRAY_NOT_ALLOWED`
1314
-
1315
- ---
1156
+ ## Error code: `EMPTY_ARRAY_NOT_ALLOWED`
1316
1157
 
1317
1158
  ### `minItems`
1318
1159
 
1319
- Defines the minimum number of items required in an array.
1320
-
1321
- Default: `Not applied when omitted.`
1160
+ Defines the minimum number of items required in an array. Default: `Not applied when omitted.`
1322
1161
 
1323
1162
  ```js
1324
1163
  const rules = {
1325
1164
  tags: {
1326
1165
  type: "array",
1327
-
1328
1166
  minItems: 2,
1329
1167
  },
1330
1168
  };
@@ -1333,37 +1171,23 @@ const rules = {
1333
1171
  An array with fewer than 2 items returns `MIN_ITEMS`.
1334
1172
 
1335
1173
  ```js
1336
-
1337
1174
  {
1338
-
1339
1175
  path: "tags",
1340
-
1341
1176
  code: "MIN_ITEMS",
1342
-
1343
1177
  message: "Attribute tags must contain at least 2 item(s)"
1344
-
1345
1178
  }
1346
1179
  ```
1347
1180
 
1348
- `minItems` is enforced even when `allowEmptyArray: true` is set. For
1349
-
1350
- example, `minItems: 2` still rejects `[]`.
1351
-
1352
- Error code: `MIN_ITEMS`
1353
-
1354
- ---
1181
+ ## `minItems` is enforced even when `allowEmptyArray: true` is set. For example, `minItems: 2` still rejects `[]`. Error code: `MIN_ITEMS`
1355
1182
 
1356
1183
  ### `maxItems`
1357
1184
 
1358
- Defines the maximum number of items allowed in an array.
1359
-
1360
- Default: `Not applied when omitted`.
1185
+ Defines the maximum number of items allowed in an array. Default: `Not applied when omitted`.
1361
1186
 
1362
1187
  ```js
1363
1188
  const rules = {
1364
1189
  tags: {
1365
1190
  type: "array",
1366
-
1367
1191
  maxItems: 5,
1368
1192
  },
1369
1193
  };
@@ -1372,23 +1196,14 @@ const rules = {
1372
1196
  An array with more than 5 items returns `MAX_ITEMS`.
1373
1197
 
1374
1198
  ```js
1375
-
1376
1199
  {
1377
-
1378
1200
  path: "tags",
1379
-
1380
1201
  code: "MAX_ITEMS",
1381
-
1382
1202
  message: "Attribute tags must contain at most 5 item(s)"
1383
-
1384
1203
  }
1385
1204
  ```
1386
1205
 
1387
- `minItems` and `maxItems` can be used together.
1388
-
1389
- Error code: `MAX_ITEMS`
1390
-
1391
- ---
1206
+ ## `minItems` and `maxItems` can be used together. Error code: `MAX_ITEMS`
1392
1207
 
1393
1208
  ### `type`
1394
1209
 
@@ -1397,33 +1212,19 @@ Validates the expected data type.
1397
1212
  Supported values:
1398
1213
 
1399
1214
  ```text
1400
-
1401
1215
  number
1402
-
1403
1216
  string
1404
-
1405
1217
  boolean
1406
-
1407
1218
  email
1408
-
1409
1219
  url
1410
-
1411
1220
  enum
1412
-
1413
1221
  uuid
1414
-
1415
1222
  uuidv1
1416
-
1417
1223
  uuidv3
1418
-
1419
1224
  uuidv4
1420
-
1421
1225
  uuidv5
1422
-
1423
1226
  objectId
1424
-
1425
1227
  array
1426
-
1427
1228
  object
1428
1229
  ```
1429
1230
 
@@ -1434,11 +1235,9 @@ const rules = {
1434
1235
  age: {
1435
1236
  type: "number",
1436
1237
  },
1437
-
1438
1238
  email: {
1439
1239
  type: "email",
1440
1240
  },
1441
-
1442
1241
  active: {
1443
1242
  type: "boolean",
1444
1243
  },
@@ -1459,7 +1258,6 @@ Example:
1459
1258
  const rules = {
1460
1259
  status: {
1461
1260
  type: "enum",
1462
-
1463
1261
  enumValues: ["active", "inactive", "blocked", 1, 0],
1464
1262
  },
1465
1263
  };
@@ -1479,7 +1277,6 @@ Example:
1479
1277
  const rules = {
1480
1278
  status: {
1481
1279
  type: "enum",
1482
-
1483
1280
  enumValues: ["active", "inactive", "blocked"],
1484
1281
  },
1485
1282
  };
@@ -1490,37 +1287,23 @@ Error code: `INVALID_ENUM`
1490
1287
  Possible error codes for types:
1491
1288
 
1492
1289
  ```text
1493
-
1494
1290
  INVALID_TYPE
1495
-
1496
1291
  INVALID_EMAIL
1497
-
1498
1292
  INVALID_URL
1499
-
1500
1293
  INVALID_ENUM
1501
-
1502
1294
  INVALID_UUID
1503
-
1504
1295
  INVALID_UUID_V1
1505
-
1506
1296
  INVALID_UUID_V3
1507
-
1508
1297
  INVALID_UUID_V4
1509
-
1510
1298
  INVALID_UUID_V5
1511
-
1512
1299
  INVALID_OBJECT_ID
1513
1300
  ```
1514
1301
 
1515
- For `type: "number"`, `NaN` is rejected as `INVALID_TYPE`.
1516
-
1517
- ---
1302
+ ## For `type: "number"`, `NaN` is rejected as `INVALID_TYPE`.
1518
1303
 
1519
1304
  ### `regex`
1520
1305
 
1521
- Validates a value using a regular expression.
1522
-
1523
- Default: `Not applied when omitted.`
1306
+ Validates a value using a regular expression. Default: `Not applied when omitted.`
1524
1307
 
1525
1308
  Example:
1526
1309
 
@@ -1528,21 +1311,16 @@ Example:
1528
1311
  const rules = {
1529
1312
  employeeCode: {
1530
1313
  type: "string",
1531
-
1532
1314
  regex: /[^1]{3}[0-9]{3}$/,
1533
1315
  },
1534
1316
  };
1535
1317
  ```
1536
1318
 
1537
- Error code: `REGEX_MISMATCH`
1538
-
1539
- ---
1319
+ ## Error code: `REGEX_MISMATCH`
1540
1320
 
1541
1321
  ### `minLength`
1542
1322
 
1543
- Defines the minimum allowed string length.
1544
-
1545
- Default: `Not applied when omitted.`
1323
+ Defines the minimum allowed string length. Default: `Not applied when omitted.`
1546
1324
 
1547
1325
  Example:
1548
1326
 
@@ -1550,21 +1328,16 @@ Example:
1550
1328
  const rules = {
1551
1329
  username: {
1552
1330
  type: "string",
1553
-
1554
1331
  minLength: 5,
1555
1332
  },
1556
1333
  };
1557
1334
  ```
1558
1335
 
1559
- Error code: `MIN_LENGTH`
1560
-
1561
- ---
1336
+ ## Error code: `MIN_LENGTH`
1562
1337
 
1563
1338
  ### `maxLength`
1564
1339
 
1565
- Defines the maximum allowed string length.
1566
-
1567
- Default: `Not applied when omitted.`
1340
+ Defines the maximum allowed string length. Default: `Not applied when omitted.`
1568
1341
 
1569
1342
  Example:
1570
1343
 
@@ -1572,21 +1345,16 @@ Example:
1572
1345
  const rules = {
1573
1346
  username: {
1574
1347
  type: "string",
1575
-
1576
1348
  maxLength: 20,
1577
1349
  },
1578
1350
  };
1579
1351
  ```
1580
1352
 
1581
- Error code: `MAX_LENGTH`
1582
-
1583
- ---
1353
+ ## Error code: `MAX_LENGTH`
1584
1354
 
1585
1355
  ### `preventDecimal`
1586
1356
 
1587
- Prevents decimal numbers.
1588
-
1589
- Default: `false`; both integer and decimal numbers are allowed.
1357
+ Prevents decimal numbers. Default: `false`; both integer and decimal numbers are allowed.
1590
1358
 
1591
1359
  Example:
1592
1360
 
@@ -1594,21 +1362,16 @@ Example:
1594
1362
  const rules = {
1595
1363
  quantity: {
1596
1364
  type: "number",
1597
-
1598
1365
  preventDecimal: true,
1599
1366
  },
1600
1367
  };
1601
1368
  ```
1602
1369
 
1603
- Error code: `DECIMAL_NOT_ALLOWED`
1604
-
1605
- ---
1370
+ ## Error code: `DECIMAL_NOT_ALLOWED`
1606
1371
 
1607
1372
  ### `min`
1608
1373
 
1609
- Defines the minimum allowed numeric value.
1610
-
1611
- Default: `Not applied when omitted.`
1374
+ Defines the minimum allowed numeric value. Default: `Not applied when omitted.`
1612
1375
 
1613
1376
  Example:
1614
1377
 
@@ -1616,21 +1379,16 @@ Example:
1616
1379
  const rules = {
1617
1380
  age: {
1618
1381
  type: "number",
1619
-
1620
1382
  min: 18,
1621
1383
  },
1622
1384
  };
1623
1385
  ```
1624
1386
 
1625
- Error code: `MIN_VALUE`
1626
-
1627
- ---
1387
+ ## Error code: `MIN_VALUE`
1628
1388
 
1629
1389
  ### `max`
1630
1390
 
1631
- Defines the maximum allowed numeric value.
1632
-
1633
- Default: `Not applied when omitted.`
1391
+ Defines the maximum allowed numeric value. Default: `Not applied when omitted.`
1634
1392
 
1635
1393
  Example:
1636
1394
 
@@ -1638,21 +1396,16 @@ Example:
1638
1396
  const rules = {
1639
1397
  quantity: {
1640
1398
  type: "number",
1641
-
1642
1399
  max: 100,
1643
1400
  },
1644
1401
  };
1645
1402
  ```
1646
1403
 
1647
- Error code: `MAX_VALUE`
1648
-
1649
- ---
1404
+ ## Error code: `MAX_VALUE`
1650
1405
 
1651
1406
  ### `range`
1652
1407
 
1653
- Defines the allowed numeric range.
1654
-
1655
- Default: `Not applied when omitted.`
1408
+ Defines the allowed numeric range. Default: `Not applied when omitted.`
1656
1409
 
1657
1410
  Example:
1658
1411
 
@@ -1660,15 +1413,12 @@ Example:
1660
1413
  const rules = {
1661
1414
  marks: {
1662
1415
  type: "number",
1663
-
1664
1416
  range: "0-100",
1665
1417
  },
1666
1418
  };
1667
1419
  ```
1668
1420
 
1669
- Error code: `OUT_OF_RANGE`
1670
-
1671
- ---
1421
+ ## Error code: `OUT_OF_RANGE`
1672
1422
 
1673
1423
  ### `elementConstraints`
1674
1424
 
@@ -1680,10 +1430,8 @@ Example:
1680
1430
  const rules = {
1681
1431
  marks: {
1682
1432
  type: "array",
1683
-
1684
1433
  elementConstraints: {
1685
1434
  type: "number",
1686
-
1687
1435
  range: "0-100",
1688
1436
  },
1689
1437
  },
@@ -1693,23 +1441,36 @@ const rules = {
1693
1441
  Example error:
1694
1442
 
1695
1443
  ```js
1696
-
1697
1444
  {
1698
-
1699
1445
  path: "marks[2]",
1700
-
1701
1446
  code: "OUT_OF_RANGE",
1447
+ message: "Attribute marks[2] should have a value between 0 and 100"
1448
+ }
1449
+ ```
1702
1450
 
1703
- message:
1704
-
1705
- "Attribute marks[2] should have a value between 0 and 100"
1451
+ Array-element transformations receive three arguments:
1706
1452
 
1707
- }
1453
+ ```js
1454
+ const rules = {
1455
+ tags: {
1456
+ type: "array",
1457
+ elementConstraints: {
1458
+ transform: (value, index, payload) => {
1459
+ return String(value).trim().toLowerCase();
1460
+ },
1461
+ type: "string",
1462
+ },
1463
+ },
1464
+ };
1708
1465
  ```
1709
1466
 
1710
- When `elementConstraintsError` is explicitly provided, the error code
1467
+ For an `elementConstraints` transform:
1468
+
1469
+ - `value` is the current array element.
1470
+ - `index` is the zero-based index of that element.
1471
+ - `payload` is the complete root payload/source object currently being validated.
1711
1472
 
1712
- is: `INVALID_ARRAY_ELEMENT`
1473
+ 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`.
1713
1474
 
1714
1475
  Example:
1715
1476
 
@@ -1717,18 +1478,14 @@ Example:
1717
1478
  const rules = {
1718
1479
  marks: {
1719
1480
  type: "array",
1720
-
1721
1481
  elementConstraints: {
1722
1482
  type: "number",
1723
1483
  },
1724
-
1725
1484
  elementConstraintsError: "Every marks element must be a number",
1726
1485
  },
1727
1486
  };
1728
1487
  ```
1729
1488
 
1730
- ---
1731
-
1732
1489
  ### `objectAttr`
1733
1490
 
1734
1491
  Validates fields inside a nested object.
@@ -1739,22 +1496,17 @@ Example:
1739
1496
  const rules = {
1740
1497
  address: {
1741
1498
  type: "object",
1742
-
1743
1499
  objectAttr: {
1744
1500
  city: {
1745
1501
  mandatory: true,
1746
-
1747
1502
  type: "string",
1748
1503
  },
1749
-
1750
1504
  location: {
1751
1505
  type: "object",
1752
-
1753
1506
  objectAttr: {
1754
1507
  latitude: {
1755
1508
  type: "number",
1756
1509
  },
1757
-
1758
1510
  longitude: {
1759
1511
  type: "number",
1760
1512
  },
@@ -1768,17 +1520,11 @@ const rules = {
1768
1520
  Nested errors include the complete field path:
1769
1521
 
1770
1522
  ```js
1771
-
1772
1523
  {
1773
-
1774
1524
  path: "address.location.latitude",
1775
-
1776
1525
  code: "INVALID_TYPE",
1777
-
1778
1526
  message:
1779
-
1780
1527
  "Invalid type for attribute address.location.latitude, required number value"
1781
-
1782
1528
  }
1783
1529
  ```
1784
1530
 
@@ -1794,14 +1540,11 @@ Example:
1794
1540
  const rules = {
1795
1541
  minSalary: {
1796
1542
  type: "number",
1797
-
1798
1543
  dependency: {
1799
1544
  maxSalary: {
1800
1545
  setDependencyRule: (minSalary, maxSalary) => ({
1801
1546
  type: "number",
1802
-
1803
1547
  min: minSalary + 1,
1804
-
1805
1548
  minError: "maxSalary must be more than minSalary",
1806
1549
  }),
1807
1550
  },
@@ -1813,21 +1556,61 @@ const rules = {
1813
1556
  Example error:
1814
1557
 
1815
1558
  ```js
1816
-
1817
1559
  {
1818
-
1819
1560
  path: "maxSalary",
1820
-
1821
1561
  code: "MIN_VALUE",
1822
-
1823
1562
  message:
1824
-
1825
1563
  "maxSalary must be more than minSalary"
1826
-
1827
1564
  }
1828
1565
  ```
1829
1566
 
1830
- ---
1567
+ ## Validation Precedence
1568
+
1569
+ v2 uses a fixed validation order. The order in which properties are written inside a rule object does not change runtime behavior.
1570
+
1571
+ ```text
1572
+ 1. mandatory / presence
1573
+ 2. transformations
1574
+ trim
1575
+ lowercase
1576
+ uppercase
1577
+ transform
1578
+ 3. allowNull
1579
+ 4. type
1580
+ 5. empty-container checks
1581
+ allowEmptyObject
1582
+ allowEmptyArray
1583
+ 6. structural rules
1584
+ objectAttr
1585
+ elementConstraints
1586
+ minItems
1587
+ maxItems
1588
+ 7. value constraints
1589
+ regex
1590
+ minLength
1591
+ maxLength
1592
+ preventDecimal
1593
+ min
1594
+ max
1595
+ range
1596
+ 8. dependency
1597
+ 9. customValidator
1598
+ ```
1599
+
1600
+ For a single field, once an earlier validation fails, later validations for that field are pruned.
1601
+
1602
+ Examples:
1603
+
1604
+ ```text
1605
+ mandatory fails
1606
+ → transform/type/customValidator do not run
1607
+ type fails
1608
+ → objectAttr/elementConstraints/regex/customValidator do not run
1609
+ regex fails
1610
+ → customValidator does not run
1611
+ ```
1612
+
1613
+ This 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.
1831
1614
 
1832
1615
  ## Array Size and Nested Validation
1833
1616
 
@@ -1841,17 +1624,12 @@ Use `minItems` and `maxItems` with `type: "array"`:
1841
1624
  const rules = {
1842
1625
  products: {
1843
1626
  type: "array",
1844
-
1845
1627
  minItems: 1,
1846
-
1847
1628
  maxItems: 3,
1848
-
1849
1629
  elementConstraints: {
1850
1630
  type: "object",
1851
-
1852
1631
  objectAttr: {
1853
1632
  productId: { mandatory: true, type: "string" },
1854
-
1855
1633
  quantity: { mandatory: true, type: "number", min: 1 },
1856
1634
  },
1857
1635
  },
@@ -1862,15 +1640,10 @@ const rules = {
1862
1640
  If the array is empty, `minItems` reports the array path itself:
1863
1641
 
1864
1642
  ```js
1865
-
1866
1643
  {
1867
-
1868
1644
  path: "products",
1869
-
1870
1645
  code: "MIN_ITEMS",
1871
-
1872
1646
  message: "Attribute products must contain at least 1 item(s)"
1873
-
1874
1647
  }
1875
1648
  ```
1876
1649
 
@@ -1879,7 +1652,6 @@ If the array is empty, `minItems` reports the array path itself:
1879
1652
  `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:
1880
1653
 
1881
1654
  ```text
1882
-
1883
1655
  products[1].quantity
1884
1656
  ```
1885
1657
 
@@ -1891,32 +1663,21 @@ products[1].quantity
1891
1663
  const rules = {
1892
1664
  orders: {
1893
1665
  type: "array",
1894
-
1895
1666
  minItems: 1,
1896
-
1897
1667
  maxItems: 2,
1898
-
1899
1668
  elementConstraints: {
1900
1669
  type: "object",
1901
-
1902
1670
  objectAttr: {
1903
1671
  orderId: { mandatory: true, type: "string" },
1904
-
1905
1672
  items: {
1906
1673
  mandatory: true,
1907
-
1908
1674
  type: "array",
1909
-
1910
1675
  minItems: 1,
1911
-
1912
1676
  maxItems: 2,
1913
-
1914
1677
  elementConstraints: {
1915
1678
  type: "object",
1916
-
1917
1679
  objectAttr: {
1918
1680
  productId: { mandatory: true, type: "string" },
1919
-
1920
1681
  quantity: { mandatory: true, type: "number", min: 1 },
1921
1682
  },
1922
1683
  },
@@ -1932,23 +1693,19 @@ A deep validation failure preserves the complete indexed path, for
1932
1693
  example:
1933
1694
 
1934
1695
  ```text
1935
-
1936
1696
  orders[1].items[2].quantity
1937
1697
  ```
1938
1698
 
1939
1699
  Array constraints work at nested levels too. A nested array can report paths such as:
1940
1700
 
1941
1701
  ```text
1942
-
1943
1702
  orders[1].items
1944
1703
  ```
1945
1704
 
1946
1705
  Nested arrays are supported and every array index is preserved:
1947
1706
 
1948
1707
  ```text
1949
-
1950
1708
  matrix[1][1]
1951
-
1952
1709
  matrix[1][1][1]
1953
1710
  ```
1954
1711
 
@@ -1956,7 +1713,7 @@ Transformations applied inside nested objects or array elements are preserved in
1956
1713
 
1957
1714
  ### Transformations and Sanitization
1958
1715
 
1959
- `perfectPayload()` can transform a field before its validation rules run. The transformed value is returned in `validatedPayload`, while the original input object is not mutated.
1716
+ `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.
1960
1717
 
1961
1718
  Supported transformation rules:
1962
1719
 
@@ -1967,66 +1724,58 @@ Supported transformation rules:
1967
1724
  | `uppercase` | Converts strings to uppercase. |
1968
1725
  | `transform` | Runs a custom synchronous transformation function. |
1969
1726
 
1970
- Transformations always run in this fixed order, regardless of the order in which the rule properties are written:
1727
+ The v2 validation flow is deterministic and does not depend on the order in which rule properties are written:
1971
1728
 
1972
1729
  ```text
1973
-
1730
+ mandatory / presence
1731
+ ↓
1974
1732
  trim
1975
-
1976
- ↓
1977
-
1733
+ ↓
1978
1734
  lowercase
1979
-
1980
- ↓
1981
-
1735
+ ↓
1982
1736
  uppercase
1983
-
1984
- ↓
1985
-
1986
- transform(value, payload)
1987
-
1988
- ↓
1989
-
1990
- validation rules
1991
-
1992
- ↓
1993
-
1737
+ ↓
1738
+ transform
1739
+ ↓
1740
+ allowNull
1741
+ ↓
1742
+ type
1743
+ ↓
1744
+ empty-container checks
1745
+ ↓
1746
+ structural validation
1747
+ ↓
1748
+ value constraints
1749
+ ↓
1750
+ dependency
1751
+ ↓
1994
1752
  customValidator
1995
-
1996
- ↓
1997
-
1753
+ ↓
1998
1754
  validatedPayload
1999
1755
  ```
2000
1756
 
1757
+ If `mandatory` fails, transformations and all later rules for that field are skipped.
1758
+
2001
1759
  #### `trim`
2002
1760
 
2003
1761
  ```js
2004
1762
  const payload = {
2005
1763
  name: " Kiran Poojary ",
2006
1764
  };
2007
-
2008
1765
  const rules = {
2009
1766
  name: {
2010
1767
  type: "string",
2011
-
2012
1768
  trim: true,
2013
1769
  },
2014
1770
  };
2015
-
2016
1771
  const result = perfectPayload(payload, rules);
2017
-
2018
1772
  console.log(result.validatedPayload.name);
2019
-
2020
1773
  // "Kiran Poojary"
2021
-
2022
1774
  console.log(payload.name);
2023
-
2024
1775
  // " Kiran Poojary "
2025
1776
  ```
2026
1777
 
2027
- `trim` applies only to string values. Non-string values are left
2028
-
2029
- unchanged.
1778
+ `trim` applies only to string values. Non-string values are left unchanged.
2030
1779
 
2031
1780
  #### `lowercase`
2032
1781
 
@@ -2034,9 +1783,7 @@ unchanged.
2034
1783
  const rules = {
2035
1784
  email: {
2036
1785
  trim: true,
2037
-
2038
1786
  lowercase: true,
2039
-
2040
1787
  type: "email",
2041
1788
  },
2042
1789
  };
@@ -2050,7 +1797,6 @@ For `" KIRAN@EXAMPLE.COM "`, the validated value becomes `"kiran@example.com"`
2050
1797
  const rules = {
2051
1798
  countryCode: {
2052
1799
  type: "string",
2053
-
2054
1800
  uppercase: true,
2055
1801
  },
2056
1802
  };
@@ -2066,15 +1812,14 @@ Use `transform` when the built-in string transformations are not enough.
2066
1812
  const rules = {
2067
1813
  phone: {
2068
1814
  type: "string",
2069
-
2070
- transform: (value) => value.replace(/`\s`{=tex}+/g, ""),
1815
+ transform: (value) => String(value).replace(/\s+/g, ""),
2071
1816
  },
2072
1817
  };
2073
1818
  ```
2074
1819
 
2075
1820
  For `"98765 43210"`, the validated value becomes `"9876543210"`.
2076
1821
 
2077
- The transformer receives two arguments:
1822
+ A normal transformer receives two arguments:
2078
1823
 
2079
1824
  ```js
2080
1825
  transform: (value, payload) => {
@@ -2083,35 +1828,38 @@ transform: (value, payload) => {
2083
1828
  ```
2084
1829
 
2085
1830
  - `value` is the field value after the built-in transformations have run.
1831
+ - `payload` is the **complete root payload/source object currently being validated**.
2086
1832
 
2087
- - `payload` is the current payload/object being validated.
1833
+ 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:
1834
+
1835
+ ```text
1836
+ body rule → request.body
1837
+ headers rule → request.headers
1838
+ params rule → request.params
1839
+ query rule → request.query
1840
+ ```
2088
1841
 
2089
- This makes cross-field transformations possible:
1842
+ This root-payload contract is preserved inside nested `objectAttr` and `elementConstraints` validation.
1843
+
1844
+ Example cross-field transformation:
2090
1845
 
2091
1846
  ```js
2092
1847
  const payload = {
2093
1848
  amount: 100,
2094
-
2095
1849
  multiplier: 2,
2096
1850
  };
2097
-
2098
1851
  const rules = {
2099
1852
  amount: {
2100
1853
  transform: (value, payload) => value * payload.multiplier,
2101
-
2102
1854
  type: "number",
2103
1855
  },
2104
-
2105
1856
  multiplier: {
2106
1857
  type: "number",
2107
1858
  },
2108
1859
  };
2109
-
2110
1860
  const result = perfectPayload(payload, rules);
2111
-
2112
1861
  console.log(result.validatedPayload.amount);
2113
-
2114
- // 200***
1862
+ // 200
2115
1863
  ```
2116
1864
 
2117
1865
  A custom transformer may also change the data type before validation:
@@ -2120,61 +1868,30 @@ A custom transformer may also change the data type before validation:
2120
1868
  const rules = {
2121
1869
  quantity: {
2122
1870
  transform: (value) => Number(value),
2123
-
2124
1871
  type: "number",
2125
-
2126
1872
  min: 1,
2127
-
2128
1873
  max: 100,
2129
1874
  },
2130
1875
  };
2131
1876
  ```
2132
1877
 
2133
- The transformed value is validated by the normal validation rules and is also the value received by `customValidator`. Transformations work inside `objectAttr` and `elementConstraints`, and transformed nested/array values are preserved in `validatedPayload`.
1878
+ For `elementConstraints`, the callback additionally receives the zero-based array index:
2134
1879
 
2135
1880
  ```js
2136
- const rules = {
2137
- profile: {
2138
- type: "object",
2139
-
2140
- objectAttr: {
2141
- name: {
2142
- trim: true,
2143
-
2144
- uppercase: true,
2145
-
2146
- type: "string",
2147
- },
2148
- },
2149
- },
2150
-
2151
- tags: {
2152
- type: "array",
2153
-
2154
- elementConstraints: {
2155
- trim: true,
2156
-
2157
- lowercase: true,
2158
-
2159
- type: "string",
2160
- },
1881
+ elementConstraints: {
1882
+ transform: (value, index, payload) => {
1883
+ return String(value).trim();
2161
1884
  },
2162
- };
1885
+ type: "string",
1886
+ }
2163
1887
  ```
2164
1888
 
2165
- Missing optional fields are not transformed. An input value of `null` is not passed to transformation functions; null handling remains controlled by `allowNull`.
2166
-
2167
- **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.
2168
-
2169
- Returning `null`, `""`, `0`, or `false` is allowed; the transformed value is then processed by the normal validation rules. Exceptions thrown inside the transformer propagate to the caller.
1889
+ 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.
2170
1890
 
2171
1891
  For example, returning `undefined` throws:
2172
1892
 
2173
1893
  ```text
2174
-
2175
- perfect-payload:- transform must not return undefined for attribute
2176
-
2177
- username
1894
+ perfect-payload:- transform must not return undefined for attribute username
2178
1895
  ```
2179
1896
 
2180
1897
  ### `customValidator`
@@ -2190,13 +1907,9 @@ customValidator: (value, payload) => {
2190
1907
  ```
2191
1908
 
2192
1909
  - `value` is the field value after transformations have been applied.
2193
-
2194
1910
  - `payload` is the current payload/object being validated.
2195
-
2196
1911
  - Return `true` to pass.
2197
-
2198
1912
  - Any value other than `true` fails validation.
2199
-
2200
1913
  - Exceptions thrown by the validator propagate to the caller.
2201
1914
 
2202
1915
  For nested validation, `payload` means the current nested object rather than the root request body.
@@ -2209,50 +1922,32 @@ Use a synchronous validator with `perfectPayload()`:
2209
1922
  const rules = {
2210
1923
  username: {
2211
1924
  mandatory: true,
2212
-
2213
1925
  type: "string",
2214
-
2215
1926
  trim: true,
2216
-
2217
1927
  customValidator: (value) => {
2218
1928
  return !value.toLowerCase().includes("admin");
2219
1929
  },
2220
-
2221
1930
  customValidatorCode: "RESERVED_USERNAME",
2222
-
2223
1931
  customValidatorError: "Username cannot contain admin",
2224
1932
  },
2225
1933
  };
2226
-
2227
1934
  const result = perfectPayload({ username: " admin_kiran " }, rules);
2228
1935
  ```
2229
1936
 
2230
1937
  A failure returns:
2231
1938
 
2232
1939
  ```js
2233
-
2234
1940
  {
2235
-
2236
1941
  statusCode: 400,
2237
-
2238
1942
  valid: false,
2239
-
2240
1943
  message: "One or more attribute values are invalid",
2241
-
2242
1944
  errors: [
2243
-
2244
1945
  {
2245
-
2246
1946
  path: "username",
2247
-
2248
1947
  code: "RESERVED_USERNAME",
2249
-
2250
1948
  message: "Username cannot contain admin"
2251
-
2252
1949
  }
2253
-
2254
1950
  ]
2255
-
2256
1951
  }
2257
1952
  ```
2258
1953
 
@@ -2263,16 +1958,12 @@ const rules = {
2263
1958
  limit: {
2264
1959
  type: "number",
2265
1960
  },
2266
-
2267
1961
  amount: {
2268
1962
  type: "number",
2269
-
2270
1963
  customValidator: (value, payload) => {
2271
1964
  return value <= payload.limit;
2272
1965
  },
2273
-
2274
1966
  customValidatorCode: "LIMIT_EXCEEDED",
2275
-
2276
1967
  customValidatorError: "Amount cannot exceed limit",
2277
1968
  },
2278
1969
  };
@@ -2283,26 +1974,17 @@ If `customValidatorCode` and `customValidatorError` are omitted, the
2283
1974
  default error is:
2284
1975
 
2285
1976
  ```js
2286
-
2287
1977
  {
2288
-
2289
1978
  path: "username",
2290
-
2291
1979
  code: "CUSTOM_VALIDATION_FAILED",
2292
-
2293
1980
  message: "Custom validation failed for attribute username"
2294
-
2295
1981
  }
2296
1982
  ```
2297
1983
 
2298
- `customValidator` works recursively inside `objectAttr` and `elementConstraints`. Structured errors preserve the corresponding nested and array paths.
2299
-
2300
- When using `perfectPayload()`, `customValidator` must remain synchronous. A Promise-returning validator throws:
1984
+ `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:
2301
1985
 
2302
1986
  ```text
2303
-
2304
1987
  perfect-payload:- customValidator must be synchronous for attribute
2305
-
2306
1988
  username
2307
1989
  ```
2308
1990
 
@@ -2310,38 +1992,27 @@ For asynchronous custom validation, use `perfectPayloadAsync()`.
2310
1992
 
2311
1993
  ## Asynchronous Validation
2312
1994
 
2313
- `perfectPayloadAsync()` supports both synchronous and asynchronous `customValidator` functions without changing the behavior of
2314
-
2315
- `perfectPayload()`.
1995
+ `perfectPayloadAsync()` supports both synchronous and asynchronous `customValidator` functions without changing the behavior of `perfectPayload()`.
2316
1996
 
2317
1997
  ```js
2318
1998
  import { perfectPayloadAsync } from "perfect-payload";
2319
-
2320
1999
  const rules = {
2321
2000
  username: {
2322
2001
  mandatory: true,
2323
-
2324
2002
  type: "string",
2325
-
2326
2003
  trim: true,
2327
-
2328
2004
  customValidator: async (value) => {
2329
2005
  const available = await checkUsernameAvailability(value);
2330
-
2331
2006
  return available;
2332
2007
  },
2333
-
2334
2008
  customValidatorCode: "USERNAME_TAKEN",
2335
-
2336
2009
  customValidatorError: "Username is already taken",
2337
2010
  },
2338
2011
  };
2339
-
2340
2012
  const result = await perfectPayloadAsync(
2341
2013
  {
2342
2014
  username: " kiran ",
2343
2015
  },
2344
-
2345
2016
  rules,
2346
2017
  );
2347
2018
  ```
@@ -2349,48 +2020,29 @@ const result = await perfectPayloadAsync(
2349
2020
  On success, transformations are preserved:
2350
2021
 
2351
2022
  ```js
2352
-
2353
2023
  {
2354
-
2355
2024
  statusCode: 200,
2356
-
2357
2025
  valid: true,
2358
-
2359
2026
  validatedPayload: {
2360
-
2361
2027
  username: "kiran"
2362
-
2363
2028
  }
2364
-
2365
2029
  }
2366
2030
  ```
2367
2031
 
2368
2032
  On asynchronous validation failure:
2369
2033
 
2370
2034
  ```js
2371
-
2372
2035
  {
2373
-
2374
2036
  statusCode: 400,
2375
-
2376
2037
  valid: false,
2377
-
2378
2038
  message: "One or more attribute values are invalid",
2379
-
2380
2039
  errors: [
2381
-
2382
2040
  {
2383
-
2384
2041
  path: "username",
2385
-
2386
2042
  code: "USERNAME_TAKEN",
2387
-
2388
2043
  message: "Username is already taken"
2389
-
2390
2044
  }
2391
-
2392
2045
  ]
2393
-
2394
2046
  }
2395
2047
  ```
2396
2048
 
@@ -2399,15 +2051,10 @@ On asynchronous validation failure:
2399
2051
  For `perfectPayloadAsync()`:
2400
2052
 
2401
2053
  ```text
2402
-
2403
2054
  true → pass
2404
-
2405
2055
  false → validation failure
2406
-
2407
2056
  anything != true → validation failure
2408
-
2409
2057
  throw → exception propagates
2410
-
2411
2058
  rejected Promise → rejection propagates
2412
2059
  ```
2413
2060
 
@@ -2419,22 +2066,17 @@ API:
2419
2066
  const rules = {
2420
2067
  username: {
2421
2068
  type: "string",
2422
-
2423
2069
  customValidator: (value) => value !== "admin",
2424
2070
  },
2425
2071
  };
2426
-
2427
2072
  const result = await perfectPayloadAsync(payload, rules);
2428
2073
  ```
2429
2074
 
2430
- A configured `customValidator` must be a function. Otherwise an error
2431
-
2432
- is
2075
+ A configured `customValidator` must be a function. Otherwise an error is
2433
2076
 
2434
2077
  thrown:
2435
2078
 
2436
2079
  ```text
2437
-
2438
2080
  perfect-payload:- customValidator must be a function for attribute username
2439
2081
  ```
2440
2082
 
@@ -2442,11 +2084,10 @@ perfect-payload:- customValidator must be a function for attribute username
2442
2084
 
2443
2085
  `perfectPayloadAsync()` uses two phases:
2444
2086
 
2445
- 1. Transform the payload and run normal synchronous validation.
2446
-
2087
+ 1. Run presence checks, transformations, and the normal deterministic synchronous validation pipeline.
2447
2088
  2. If phase 1 succeeds, run custom validators with `await`.
2448
2089
 
2449
- If any synchronous validation error exists, phase 2 is skipped and the synchronous validation result is returned immediately. This means asynchronous validators can assume the payload has already passed its normal synchronous validation rules.
2090
+ 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.
2450
2091
 
2451
2092
  ### Nested async validation
2452
2093
 
@@ -2456,19 +2097,14 @@ Async custom validators work recursively inside `objectAttr`:
2456
2097
  const rules = {
2457
2098
  profile: {
2458
2099
  type: "object",
2459
-
2460
2100
  objectAttr: {
2461
2101
  username: {
2462
2102
  type: "string",
2463
-
2464
2103
  trim: true,
2465
-
2466
2104
  customValidator: async (value) => {
2467
2105
  return await isUsernameAvailable(value);
2468
2106
  },
2469
-
2470
2107
  customValidatorCode: "USERNAME_TAKEN",
2471
-
2472
2108
  customValidatorError: "Username is already taken",
2473
2109
  },
2474
2110
  },
@@ -2479,7 +2115,6 @@ const rules = {
2479
2115
  A failure produces the complete path:
2480
2116
 
2481
2117
  ```text
2482
-
2483
2118
  profile.username
2484
2119
  ```
2485
2120
 
@@ -2489,18 +2124,13 @@ They also work inside `elementConstraints`:
2489
2124
  const rules = {
2490
2125
  usernames: {
2491
2126
  type: "array",
2492
-
2493
2127
  elementConstraints: {
2494
2128
  type: "string",
2495
-
2496
2129
  trim: true,
2497
-
2498
2130
  customValidator: async (value) => {
2499
2131
  return await isUsernameAvailable(value);
2500
2132
  },
2501
-
2502
2133
  customValidatorCode: "USERNAME_TAKEN",
2503
-
2504
2134
  customValidatorError: "Username is already taken",
2505
2135
  },
2506
2136
  },
@@ -2510,7 +2140,6 @@ const rules = {
2510
2140
  For an invalid second element:
2511
2141
 
2512
2142
  ```text
2513
-
2514
2143
  usernames[1]
2515
2144
  ```
2516
2145
 
@@ -2519,9 +2148,7 @@ Deep combinations of objects and arrays preserve every level of the
2519
2148
  path:
2520
2149
 
2521
2150
  ```text
2522
-
2523
2151
  products[1].seller.username
2524
-
2525
2152
  profile.teams[1].members[1].username
2526
2153
  ```
2527
2154
 
@@ -2530,23 +2157,16 @@ Default async custom-validation messages also use the final indexed
2530
2157
  path:
2531
2158
 
2532
2159
  ```js
2533
-
2534
2160
  {
2535
-
2536
2161
  path: "users[1].username",
2537
-
2538
2162
  code: "CUSTOM_VALIDATION_FAILED",
2539
-
2540
2163
  message: "Custom validation failed for attribute users[1].username"
2541
-
2542
2164
  }
2543
2165
  ```
2544
2166
 
2545
2167
  ### Transform remains synchronous
2546
2168
 
2547
- `perfectPayloadAsync()` makes custom validation asynchronous; it does
2548
-
2549
- not make `transform` asynchronous.
2169
+ `perfectPayloadAsync()` makes custom validation asynchronous; it does not make `transform` asynchronous.
2550
2170
 
2551
2171
  `transform` must still be synchronous:
2552
2172
 
@@ -2556,9 +2176,7 @@ transform: (value, payload) => {
2556
2176
  };
2557
2177
  ```
2558
2178
 
2559
- An async transformer or a transformer that returns a Promise is not
2560
-
2561
- supported.
2179
+ An async transformer or a transformer that returns a Promise is not supported.
2562
2180
 
2563
2181
  ## Error Codes
2564
2182
 
@@ -2567,57 +2185,31 @@ supported.
2567
2185
  validation error codes:
2568
2186
 
2569
2187
  ```text
2570
-
2571
2188
  REQUIRED
2572
-
2573
2189
  NULL_NOT_ALLOWED
2574
-
2575
2190
  EMPTY_OBJECT_NOT_ALLOWED
2576
-
2577
2191
  EMPTY_ARRAY_NOT_ALLOWED
2578
-
2579
2192
  MIN_ITEMS
2580
-
2581
2193
  MAX_ITEMS
2582
-
2583
2194
  INVALID_ARRAY_ELEMENT
2584
-
2585
2195
  REGEX_MISMATCH
2586
-
2587
2196
  INVALID_TYPE
2588
-
2589
2197
  INVALID_EMAIL
2590
-
2591
2198
  INVALID_URL
2592
-
2593
2199
  INVALID_ENUM
2594
-
2595
2200
  INVALID_UUID
2596
-
2597
2201
  INVALID_UUID_V1
2598
-
2599
2202
  INVALID_UUID_V3
2600
-
2601
2203
  INVALID_UUID_V4
2602
-
2603
2204
  INVALID_UUID_V5
2604
-
2605
2205
  INVALID_OBJECT_ID
2606
-
2607
2206
  MIN_LENGTH
2608
-
2609
2207
  MAX_LENGTH
2610
-
2611
2208
  DECIMAL_NOT_ALLOWED
2612
-
2613
2209
  MIN_VALUE
2614
-
2615
2210
  MAX_VALUE
2616
-
2617
2211
  OUT_OF_RANGE
2618
-
2619
2212
  CUSTOM_VALIDATION_FAILED
2620
-
2621
2213
  UNKNOWN_FIELD
2622
2214
  ```
2623
2215
 
@@ -2626,23 +2218,14 @@ These codes are designed for programmatic handling while `message` remains suita
2626
2218
  For example:
2627
2219
 
2628
2220
  ```js
2629
-
2630
2221
  const result = perfectPayload(payload, validationRules);
2631
-
2632
2222
  if (!result.valid) {
2633
-
2634
2223
  const emailError = result.errors.find(
2635
-
2636
2224
  (error) => error.code === "INVALID_EMAIL",
2637
-
2638
2225
  );
2639
-
2640
2226
  if (emailError) {
2641
-
2642
- *****// Handle invalid email*****
2643
-
2227
+ // Handle invalid email
2644
2228
  }
2645
-
2646
2229
  }
2647
2230
  ```
2648
2231
 
@@ -2651,15 +2234,10 @@ if (!result.valid) {
2651
2234
  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:
2652
2235
 
2653
2236
  ```js
2654
-
2655
2237
  {
2656
-
2657
2238
  path: "email",
2658
-
2659
2239
  code: "INVALID_EMAIL",
2660
-
2661
2240
  message: "Email address is invalid"
2662
-
2663
2241
  }
2664
2242
  ```
2665
2243
 
@@ -2669,11 +2247,8 @@ Example:
2669
2247
  const rules = {
2670
2248
  email: {
2671
2249
  mandatory: true,
2672
-
2673
2250
  type: "email",
2674
-
2675
2251
  mandatoryError: "Email is required",
2676
-
2677
2252
  typeError: "Email address is invalid",
2678
2253
  },
2679
2254
  };
@@ -2682,30 +2257,20 @@ const rules = {
2682
2257
  If `email` is missing:
2683
2258
 
2684
2259
  ```js
2685
-
2686
2260
  {
2687
-
2688
2261
  path: "email",
2689
-
2690
2262
  code: "REQUIRED",
2691
-
2692
2263
  message: "Email is required"
2693
-
2694
2264
  }
2695
2265
  ```
2696
2266
 
2697
2267
  If `email` is present but invalid:
2698
2268
 
2699
2269
  ```js
2700
-
2701
2270
  {
2702
-
2703
2271
  path: "email",
2704
-
2705
2272
  code: "INVALID_EMAIL",
2706
-
2707
2273
  message: "Email address is invalid"
2708
-
2709
2274
  }
2710
2275
  ```
2711
2276
 
@@ -2732,101 +2297,60 @@ If `email` is present but invalid:
2732
2297
  ```js
2733
2298
  const payload = {
2734
2299
  username: "ab",
2735
-
2736
2300
  age: 15,
2737
-
2738
2301
  score: 120,
2739
2302
  };
2740
-
2741
2303
  const rules = {
2742
2304
  username: {
2743
2305
  mandatory: true,
2744
-
2745
2306
  type: "string",
2746
-
2747
2307
  minLength: 3,
2748
-
2749
2308
  mandatoryError: "Username is required",
2750
-
2751
2309
  typeError: "Username must be a string",
2752
-
2753
2310
  minLengthError: "Username must contain at least 3 characters",
2754
2311
  },
2755
-
2756
2312
  age: {
2757
2313
  type: "number",
2758
-
2759
2314
  min: 18,
2760
-
2761
2315
  minError: "Age must be at least 18",
2762
2316
  },
2763
-
2764
2317
  score: {
2765
2318
  type: "number",
2766
-
2767
2319
  range: "0-100",
2768
-
2769
2320
  rangeError: "Score must be between 0 and 100",
2770
2321
  },
2771
2322
  };
2772
-
2773
2323
  const result = perfectPayload(payload, rules);
2774
2324
  ```
2775
2325
 
2776
2326
  Example result:
2777
2327
 
2778
2328
  ```js
2779
-
2780
2329
  {
2781
-
2782
2330
  statusCode: 400,
2783
-
2784
2331
  valid: false,
2785
-
2786
2332
  message:
2787
-
2788
2333
  "One or more attribute values are invalid",
2789
-
2790
2334
  errors: [
2791
-
2792
2335
  {
2793
-
2794
2336
  path: "username",
2795
-
2796
2337
  code: "MIN_LENGTH",
2797
-
2798
2338
  message:
2799
-
2800
2339
  "Username must contain at least 3 characters"
2801
-
2802
2340
  },
2803
-
2804
2341
  {
2805
-
2806
2342
  path: "age",
2807
-
2808
2343
  code: "MIN_VALUE",
2809
-
2810
2344
  message:
2811
-
2812
2345
  "Age must be at least 18"
2813
-
2814
2346
  },
2815
-
2816
2347
  {
2817
-
2818
2348
  path: "score",
2819
-
2820
2349
  code: "OUT_OF_RANGE",
2821
-
2822
2350
  message:
2823
-
2824
2351
  "Score must be between 0 and 100"
2825
-
2826
2352
  }
2827
-
2828
2353
  ]
2829
-
2830
2354
  }
2831
2355
  ```
2832
2356
 
@@ -2840,9 +2364,7 @@ For example:
2840
2364
  const rules = {
2841
2365
  age: {
2842
2366
  type: "number",
2843
-
2844
2367
  min: 18,
2845
-
2846
2368
  minError: "You must be 18 or older",
2847
2369
  },
2848
2370
  };
@@ -2851,44 +2373,31 @@ const rules = {
2851
2373
  Still returns:
2852
2374
 
2853
2375
  ```js
2854
-
2855
2376
  {
2856
-
2857
2377
  path: "age",
2858
-
2859
2378
  code: "MIN_VALUE",
2860
-
2861
2379
  message: "You must be 18 or older"
2862
-
2863
2380
  }
2864
2381
  ```
2865
2382
 
2866
2383
  This makes it possible to:
2867
2384
 
2868
2385
  - show custom messages to API consumers
2869
-
2870
2386
  - use stable error codes in application logic
2871
-
2872
2387
  - change user-facing wording without changing programmatic error
2873
2388
 
2874
2389
  handling
2875
2390
 
2876
2391
  ## Custom Response Objects
2877
2392
 
2878
- Custom valid and invalid response objects are configured inside the
2879
-
2880
- optional third `options` argument.
2393
+ Custom valid and invalid response objects are configured inside the optional third `options` argument.
2881
2394
 
2882
2395
  ```js
2883
-
2884
2396
  perfectPayload(data, validationRules, options?)
2885
-
2886
2397
  await perfectPayloadAsync(data, validationRules, options?)
2887
2398
  ```
2888
2399
 
2889
- This keeps API-level configuration in one place and avoids positional
2890
-
2891
- `undefined` arguments.
2400
+ This keeps API-level configuration in one place and avoids positional `undefined` arguments.
2892
2401
 
2893
2402
  ### Custom Valid Response
2894
2403
 
@@ -2896,9 +2405,7 @@ This keeps API-level configuration in one place and avoids positional
2896
2405
  const result = perfectPayload(payload, validationRules, {
2897
2406
  validPayloadResponse: {
2898
2407
  statusCode: 201,
2899
-
2900
2408
  valid: true,
2901
-
2902
2409
  message: "Payload validated successfully",
2903
2410
  },
2904
2411
  });
@@ -2907,25 +2414,15 @@ const result = perfectPayload(payload, validationRules, {
2907
2414
  When validation succeeds, `validatedPayload` is automatically added:
2908
2415
 
2909
2416
  ```js
2910
-
2911
2417
  {
2912
-
2913
2418
  statusCode: 201,
2914
-
2915
2419
  valid: true,
2916
-
2917
2420
  message: "Payload validated successfully",
2918
-
2919
2421
  validatedPayload: {
2920
-
2921
2422
  name: "Kiran",
2922
-
2923
2423
  email: "kiran@example.com",
2924
-
2925
2424
  age: 29
2926
-
2927
2425
  }
2928
-
2929
2426
  }
2930
2427
  ```
2931
2428
 
@@ -2935,9 +2432,7 @@ When validation succeeds, `validatedPayload` is automatically added:
2935
2432
  const result = perfectPayload(payload, validationRules, {
2936
2433
  inValidPayloadResponse: {
2937
2434
  statusCode: 422,
2938
-
2939
2435
  valid: false,
2940
-
2941
2436
  message: "Payload validation failed",
2942
2437
  },
2943
2438
  });
@@ -2946,29 +2441,17 @@ const result = perfectPayload(payload, validationRules, {
2946
2441
  When validation fails, `errors` is automatically added:
2947
2442
 
2948
2443
  ```js
2949
-
2950
2444
  {
2951
-
2952
2445
  statusCode: 422,
2953
-
2954
2446
  valid: false,
2955
-
2956
2447
  message: "Payload validation failed",
2957
-
2958
2448
  errors: [
2959
-
2960
2449
  {
2961
-
2962
2450
  path: "email",
2963
-
2964
2451
  code: "INVALID_EMAIL",
2965
-
2966
2452
  message: "Invalid email format for attribute email"
2967
-
2968
2453
  }
2969
-
2970
2454
  ]
2971
-
2972
2455
  }
2973
2456
  ```
2974
2457
 
@@ -2978,17 +2461,12 @@ When validation fails, `errors` is automatically added:
2978
2461
  const result = perfectPayload(payload, validationRules, {
2979
2462
  validPayloadResponse: {
2980
2463
  statusCode: 201,
2981
-
2982
2464
  valid: true,
2983
-
2984
2465
  message: "CUSTOM_VALID_RESPONSE",
2985
2466
  },
2986
-
2987
2467
  inValidPayloadResponse: {
2988
2468
  statusCode: 422,
2989
-
2990
2469
  valid: false,
2991
-
2992
2470
  message: "CUSTOM_INVALID_RESPONSE",
2993
2471
  },
2994
2472
  });
@@ -2999,35 +2477,26 @@ You can combine response customization with other API options:
2999
2477
  ```js
3000
2478
  const result = perfectPayload(payload, validationRules, {
3001
2479
  unknownFields: "reject",
3002
-
3003
2480
  validPayloadResponse: {
3004
2481
  statusCode: 201,
3005
-
3006
2482
  valid: true,
3007
2483
  },
3008
-
3009
2484
  inValidPayloadResponse: {
3010
2485
  statusCode: 422,
3011
-
3012
2486
  valid: false,
3013
-
3014
2487
  message: "Payload validation failed",
3015
2488
  },
3016
2489
  });
3017
2490
  ```
3018
2491
 
3019
- The response object you provide is preserved while `perfectPayload()` automatically adds `validatedPayload` for successful validation or `errors` for failed validation.
3020
-
3021
- The same response options are supported by `perfectPayloadAsync()`.
2492
+ 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()`.
3022
2493
 
3023
- ## v1.7 API Migration
2494
+ ## Migrating to the v2 API
3024
2495
 
3025
- The current `perfectPayload()` and `perfectPayloadAsync()` APIs use one optional third argument for configuration:
2496
+ The v2 `perfectPayload()` and `perfectPayloadAsync()` APIs use one optional third argument for configuration:
3026
2497
 
3027
2498
  ```js
3028
-
3029
2499
  perfectPayload(data, validationRules, options?)
3030
-
3031
2500
  perfectPayloadAsync(data, validationRules, options?)
3032
2501
  ```
3033
2502
 
@@ -3038,7 +2507,6 @@ Use:
3038
2507
  ```js
3039
2508
  perfectPayload(payload, rules, {
3040
2509
  validPayloadResponse: customValidResponse,
3041
-
3042
2510
  inValidPayloadResponse: customInvalidResponse,
3043
2511
  });
3044
2512
  ```
@@ -3048,12 +2516,9 @@ instead of passing custom response objects as separate positional arguments. Thi
3048
2516
  ```js
3049
2517
  perfectPayload(payload, rules, {
3050
2518
  unknownFields: "reject",
3051
-
3052
2519
  inValidPayloadResponse: {
3053
2520
  statusCode: 422,
3054
-
3055
2521
  valid: false,
3056
-
3057
2522
  message: "Payload validation failed",
3058
2523
  },
3059
2524
  });
@@ -3066,48 +2531,29 @@ perfectPayload(payload, rules, {
3066
2531
  If no custom response objects are provided, the default valid response is:
3067
2532
 
3068
2533
  ```js
3069
-
3070
2534
  {
3071
-
3072
2535
  statusCode: 200,
3073
-
3074
2536
  valid: true,
3075
-
3076
2537
  validatedPayload: {
3077
-
3078
- *****// validated fields*****
3079
-
2538
+ // validated fields
3080
2539
  }
3081
-
3082
2540
  }
3083
2541
  ```
3084
2542
 
3085
2543
  The default invalid response is:
3086
2544
 
3087
2545
  ```js
3088
-
3089
2546
  {
3090
-
3091
2547
  statusCode: 400,
3092
-
3093
2548
  valid: false,
3094
-
3095
2549
  message: "One or more attribute values are invalid",
3096
-
3097
2550
  errors: [
3098
-
3099
2551
  {
3100
-
3101
2552
  path: "field",
3102
-
3103
2553
  code: "ERROR_CODE",
3104
-
3105
2554
  message: "Validation error message"
3106
-
3107
2555
  }
3108
-
3109
2556
  ]
3110
-
3111
2557
  }
3112
2558
  ```
3113
2559
 
@@ -3128,15 +2574,10 @@ const payload = {
3128
2574
  An error can be returned as:
3129
2575
 
3130
2576
  ```js
3131
-
3132
2577
  {
3133
-
3134
2578
  path: "email",
3135
-
3136
2579
  code: "INVALID_EMAIL",
3137
-
3138
2580
  message: "Invalid email format for attribute email"
3139
-
3140
2581
  }
3141
2582
  ```
3142
2583
 
@@ -3148,32 +2589,25 @@ Use `objectAttr` to validate properties inside an object.
3148
2589
  const payload = {
3149
2590
  address: {
3150
2591
  city: "Bengaluru",
3151
-
3152
2592
  location: {
3153
2593
  latitude: "12.9716",
3154
-
3155
2594
  longitude: 77.5946,
3156
2595
  },
3157
2596
  },
3158
2597
  };
3159
-
3160
2598
  const rules = {
3161
2599
  address: {
3162
2600
  type: "object",
3163
-
3164
2601
  objectAttr: {
3165
2602
  city: {
3166
2603
  type: "string",
3167
2604
  },
3168
-
3169
2605
  location: {
3170
2606
  type: "object",
3171
-
3172
2607
  objectAttr: {
3173
2608
  latitude: {
3174
2609
  type: "number",
3175
2610
  },
3176
-
3177
2611
  longitude: {
3178
2612
  type: "number",
3179
2613
  },
@@ -3182,35 +2616,25 @@ const rules = {
3182
2616
  },
3183
2617
  },
3184
2618
  };
3185
-
3186
2619
  const result = perfectPayload(payload, rules);
3187
2620
  ```
3188
2621
 
3189
2622
  Because `latitude` is a string instead of a number, the error contains its complete nested path:
3190
2623
 
3191
2624
  ```js
3192
-
3193
2625
  {
3194
-
3195
2626
  path: "address.location.latitude",
3196
-
3197
2627
  code: "INVALID_TYPE",
3198
-
3199
2628
  message:
3200
-
3201
2629
  "Invalid type for attribute address.location.latitude, required number value"
3202
-
3203
2630
  }
3204
2631
  ```
3205
2632
 
3206
2633
  Nested paths use dot notation:
3207
2634
 
3208
2635
  ```text
3209
-
3210
2636
  address.city
3211
-
3212
2637
  address.location.latitude
3213
-
3214
2638
  address.location.longitude
3215
2639
  ```
3216
2640
 
@@ -3222,47 +2646,34 @@ When `elementConstraints` validation fails, the array index is included in the e
3222
2646
  const payload = {
3223
2647
  marks: [50, 75, 150],
3224
2648
  };
3225
-
3226
2649
  const rules = {
3227
2650
  marks: {
3228
2651
  type: "array",
3229
-
3230
2652
  elementConstraints: {
3231
2653
  type: "number",
3232
-
3233
2654
  range: "0-100",
3234
2655
  },
3235
2656
  },
3236
2657
  };
3237
-
3238
2658
  const result = perfectPayload(payload, rules);
3239
2659
  ```
3240
2660
 
3241
2661
  The invalid third element is reported as:
3242
2662
 
3243
2663
  ```js
3244
-
3245
2664
  {
3246
-
3247
2665
  path: "marks[2]",
3248
-
3249
2666
  code: "OUT_OF_RANGE",
3250
-
3251
2667
  message:
3252
-
3253
2668
  "Attribute marks[2] should have a value between 0 and 100"
3254
-
3255
2669
  }
3256
2670
  ```
3257
2671
 
3258
2672
  Array paths use zero-based indexes:
3259
2673
 
3260
2674
  ```text
3261
-
3262
2675
  marks[0]
3263
-
3264
2676
  marks[1]
3265
-
3266
2677
  marks[2]
3267
2678
  ```
3268
2679
 
@@ -3275,11 +2686,8 @@ Paths can also identify fields inside array elements.
3275
2686
  For example:
3276
2687
 
3277
2688
  ```text
3278
-
3279
2689
  products[0].quantity
3280
-
3281
2690
  products[1].quantity
3282
-
3283
2691
  products[2].price
3284
2692
  ```
3285
2693
 
@@ -3297,7 +2705,6 @@ For example:
3297
2705
 
3298
2706
  ```js
3299
2707
  const result = perfectPayload(payload, validationRules);
3300
-
3301
2708
  if (!result.valid) {
3302
2709
  result.errors.forEach((error) => {
3303
2710
  console.log(error.path, error.code, error.message);
@@ -3309,7 +2716,6 @@ A frontend can also map validation errors by path:
3309
2716
 
3310
2717
  ```js
3311
2718
  const fieldErrors = {};
3312
-
3313
2719
  result.errors.forEach((error) => {
3314
2720
  fieldErrors[error.path] = error.message;
3315
2721
  });
@@ -3318,15 +2724,10 @@ result.errors.forEach((error) => {
3318
2724
  Result:
3319
2725
 
3320
2726
  ```js
3321
-
3322
2727
  {
3323
-
3324
2728
  "email": "Invalid email format for attribute email",
3325
-
3326
2729
  "address.location.latitude": "Invalid type for attribute address.location.latitude, required number value",
3327
-
3328
2730
  "marks[2]": "Attribute marks[2] should have a value between 0 and 100"
3329
-
3330
2731
  }
3331
2732
  ```
3332
2733
 
@@ -3337,340 +2738,175 @@ Result:
3337
2738
  sample-1
3338
2739
 
3339
2740
  ```js
3340
-
3341
2741
  {
3342
-
3343
2742
  firstName: {
3344
-
3345
2743
  mandatory: true,
3346
-
3347
2744
  allowNull: false,
3348
-
3349
2745
  type: "string",
3350
-
3351
2746
  minLength: 3,
3352
-
3353
2747
  minLengthError: "First name must have minimum 3 characters."
3354
-
3355
2748
  },
3356
-
3357
2749
  lastName: {
3358
-
3359
2750
  mandatory: false,
3360
-
3361
2751
  allowNull: true,
3362
-
3363
2752
  type: "string",
3364
-
3365
2753
  },
3366
-
3367
2754
  email: {
3368
-
3369
2755
  mandatory: true,
3370
-
3371
2756
  allowNull: false,
3372
-
3373
2757
  type: "email",
3374
-
3375
2758
  },
3376
-
3377
2759
  phone: {
3378
-
3379
2760
  mandatory: true,
3380
-
3381
2761
  allowNull: false,
3382
-
3383
2762
  type: "string",
3384
-
3385
2763
  },
3386
-
3387
2764
  age: {
3388
-
3389
2765
  mandatory: false,
3390
-
3391
2766
  type: "number",
3392
-
3393
2767
  min: 1,
3394
-
3395
2768
  max: 120,
3396
-
3397
2769
  },
3398
-
3399
2770
  };
3400
2771
  ```
3401
2772
 
3402
2773
  sample-2
3403
2774
 
3404
2775
  ```js
3405
-
3406
2776
  {
3407
-
3408
2777
  id: {
3409
-
3410
2778
  mandatory: true,
3411
-
3412
2779
  allowNull: true,
3413
-
3414
2780
  type: "uuidv4",
3415
-
3416
2781
  },
3417
-
3418
2782
  batchId: {
3419
-
3420
2783
  mandatory: true,
3421
-
3422
2784
  allowNull: true,
3423
-
3424
2785
  type: "objectId",
3425
-
3426
2786
  },
3427
-
3428
2787
  firstName: {
3429
-
3430
2788
  mandatory: true,
3431
-
3432
2789
  type: "string",
3433
-
3434
2790
  minLength: 3,
3435
-
3436
2791
  },
3437
-
3438
2792
  lastName: {
3439
-
3440
2793
  mandatory: false,
3441
-
3442
2794
  allowNull: true,
3443
-
3444
2795
  type: "string",
3445
-
3446
2796
  },
3447
-
3448
2797
  age: {
3449
-
3450
2798
  type: "number",
3451
-
3452
2799
  min: 0.1,
3453
-
3454
2800
  max: 120,
3455
-
3456
2801
  },
3457
-
3458
2802
  isAdult: {
3459
-
3460
2803
  type: "boolean",
3461
-
3462
2804
  },
3463
-
3464
2805
  totalWins: {
3465
-
3466
2806
  type: "number",
3467
-
3468
2807
  min: 0,
3469
-
3470
2808
  preventDecimal: true,
3471
-
3472
2809
  },
3473
-
3474
2810
  email: {
3475
-
3476
2811
  regex: /[^2]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/,
3477
-
3478
2812
  },
3479
-
3480
2813
  githubLink: {
3481
-
3482
2814
  type: "url",
3483
-
3484
2815
  },
3485
-
3486
2816
  accountStatus: {
3487
-
3488
2817
  type: "enum",
3489
-
3490
2818
  enumValues: ["Active", "Inactive", 200],
3491
-
3492
2819
  },
3493
-
3494
2820
  marks: {
3495
-
3496
2821
  range: "0-100",
3497
-
3498
2822
  },
3499
-
3500
2823
  allMarks: {
3501
-
3502
2824
  type: "array",
3503
-
3504
2825
  allowEmptyArray: false,
3505
-
3506
2826
  elementConstraints: {
3507
-
3508
2827
  type: "number",
3509
-
3510
2828
  allowNull: false,
3511
-
3512
2829
  range: "0-100",
3513
-
3514
2830
  },
3515
-
3516
2831
  },
3517
-
3518
2832
  totalScore: {
3519
-
3520
2833
  type: "number",
3521
-
3522
2834
  dependency: {
3523
-
3524
2835
  result: {
3525
-
3526
2836
  setDependencyRule: (totalScore, result) => {
3527
-
3528
2837
  return { mandatory: true, allowNull: false, type: "string" };
3529
-
3530
2838
  },
3531
-
3532
2839
  },
3533
-
3534
2840
  },
3535
-
3536
2841
  },
3537
-
3538
2842
  result: {
3539
-
3540
2843
  type: "string",
3541
-
3542
2844
  dependency: {
3543
-
3544
2845
  totalScore: {
3545
-
3546
2846
  setDependencyRule: (result, totalScore) => {
3547
-
3548
2847
  return { mandatory: true, allowNull: false, type: "number" };
3549
-
3550
2848
  },
3551
-
3552
2849
  },
3553
-
3554
2850
  },
3555
-
3556
2851
  },
3557
-
3558
2852
  minSalary: {
3559
-
3560
2853
  mandatory: true,
3561
-
3562
2854
  min: 1,
3563
-
3564
2855
  type: "number",
3565
-
3566
2856
  dependency: {
3567
-
3568
2857
  maxSalary: {
3569
-
3570
2858
  setDependencyRule: (minSalary, maxSalary) => {
3571
-
3572
2859
  return {
3573
-
3574
2860
  mandatory: true,
3575
-
3576
2861
  min: minSalary + 1,
3577
-
3578
2862
  minError: "maxSalary must be more than minSalary",
3579
-
3580
2863
  };
3581
-
3582
2864
  },
3583
-
3584
2865
  },
3585
-
3586
2866
  },
3587
-
3588
2867
  },
3589
-
3590
2868
  maxSalary: {
3591
-
3592
2869
  dependency: {
3593
-
3594
2870
  minSalary: {
3595
-
3596
2871
  setDependencyRule: (maxSalary, minSalary) => {
3597
-
3598
2872
  return {
3599
-
3600
2873
  mandatory: true,
3601
-
3602
2874
  max: maxSalary - 1,
3603
-
3604
2875
  maxError: "minSalary must be less than maxSalary",
3605
-
3606
2876
  };
3607
-
3608
2877
  },
3609
-
3610
2878
  },
3611
-
3612
2879
  },
3613
-
3614
2880
  },
3615
-
3616
2881
  address: {
3617
-
3618
2882
  mandatory: true,
3619
-
3620
2883
  type: "object",
3621
-
3622
2884
  allowEmptyObject: false,
3623
-
3624
2885
  objectAttr: {
3625
-
3626
2886
  country: { mandatory: true, type: "string" },
3627
-
3628
2887
  state: {
3629
-
3630
2888
  mandatory: true,
3631
-
3632
2889
  type: "string",
3633
-
3634
2890
  },
3635
-
3636
2891
  city: {},
3637
-
3638
2892
  zip: {
3639
-
3640
2893
  mandatory: true,
3641
-
3642
2894
  type: "string",
3643
-
3644
2895
  },
3645
-
3646
2896
  position: {
3647
-
3648
2897
  mandatory: true,
3649
-
3650
2898
  type: "object",
3651
-
3652
2899
  allowEmptyObject: false,
3653
-
3654
2900
  objectAttr: {
3655
-
3656
2901
  lattitude: { mandatory: true, type: "number" },
3657
-
3658
2902
  longitude: {
3659
-
3660
2903
  mandatory: true,
3661
-
3662
2904
  type: "number",
3663
-
3664
2905
  },
3665
-
3666
2906
  },
3667
-
3668
2907
  },
3669
-
3670
2908
  },
3671
-
3672
2909
  },
3673
-
3674
2910
  }
3675
2911
  ```
3676
2912
 
@@ -3679,19 +2915,11 @@ sample-2
3679
2915
  #### Creating a route with payload validation middleware
3680
2916
 
3681
2917
  ```js
3682
-
3683
- ****// validatePayload is the middleware that invokes
3684
-
3685
- perfectPayload()****
3686
-
2918
+ // validatePayload is the middleware that invokes perfectPayload()
3687
2919
  router.post(
3688
-
3689
2920
  "/payload-validation",
3690
-
3691
2921
  validatePayload({ rule: <your validation rule json object> }),
3692
-
3693
2922
  (req, res) => res.send("OK")
3694
-
3695
2923
  );
3696
2924
  ```
3697
2925
 
@@ -3699,24 +2927,16 @@ router.post(
3699
2927
 
3700
2928
  ```js
3701
2929
  import { perfectPayload } from "perfect-payload";
3702
-
3703
2930
  export const validatePayload = ({ rule }) => {
3704
2931
  return (req, res, next) => {
3705
2932
  try {
3706
- const { statusCode, ...response } = perfectPayload(
3707
- req?.body,
3708
-
3709
- rule,
3710
- );
3711
-
2933
+ const { statusCode, ...response } = perfectPayload(req?.body, rule);
3712
2934
  if (+statusCode >= 200 && +statusCode <= 299) {
3713
2935
  req.validatedBody = response?.validatedPayload;
3714
-
3715
2936
  next();
3716
2937
  } else res.status(statusCode).json(response);
3717
2938
  } catch (error) {
3718
2939
  console.error("Error validating payload", error);
3719
-
3720
2940
  res.status(500).json({ error: "Internal Server Error" });
3721
2941
  }
3722
2942
  };
@@ -3729,26 +2949,21 @@ When your schema contains an asynchronous `customValidator`, the middleware itse
3729
2949
 
3730
2950
  ```js
3731
2951
  import { perfectPayloadAsync } from "perfect-payload";
3732
-
3733
2952
  export const validatePayloadAsync = ({ rule }) => {
3734
2953
  return async (req, res, next) => {
3735
2954
  try {
3736
2955
  const { statusCode, ...response } = await perfectPayloadAsync(
3737
2956
  req?.body,
3738
-
3739
2957
  rule,
3740
2958
  );
3741
-
3742
2959
  if (+statusCode >= 200 && +statusCode <= 299) {
3743
2960
  req.validatedBody = response?.validatedPayload;
3744
-
3745
2961
  next();
3746
2962
  } else {
3747
2963
  res.status(statusCode).json(response);
3748
2964
  }
3749
2965
  } catch (error) {
3750
2966
  console.error("Error validating payload", error);
3751
-
3752
2967
  res.status(500).json({ error: "Internal Server Error" });
3753
2968
  }
3754
2969
  };
@@ -3758,15 +2973,10 @@ export const validatePayloadAsync = ({ rule }) => {
3758
2973
  Route usage:
3759
2974
 
3760
2975
  ```js
3761
-
3762
2976
  router.post(
3763
-
3764
2977
  "/payload-validation",
3765
-
3766
2978
  validatePayloadAsync({ rule: <your validation rule json object> }),
3767
-
3768
2979
  (req, res) => res.send("OK"),
3769
-
3770
2980
  );
3771
2981
  ```
3772
2982
 
@@ -3777,35 +2987,22 @@ function validatePayload({ rule }) {
3777
2987
  return async (req, res, next) => {
3778
2988
  try {
3779
2989
  const { perfectPayload } = await import("perfect-payload");
3780
-
3781
- const { statusCode, ...response } = perfectPayload(
3782
- req?.body,
3783
-
3784
- rule,
3785
- );
3786
-
2990
+ const { statusCode, ...response } = perfectPayload(req?.body, rule);
3787
2991
  if (+statusCode >= 200 && +statusCode <= 299) {
3788
2992
  req.validatedBody = response?.validatedPayload;
3789
-
3790
2993
  next();
3791
2994
  } else {
3792
2995
  res.status(statusCode).json(response);
3793
2996
  }
3794
2997
  } catch (error) {
3795
2998
  console.error("Error validating payload", error);
3796
-
3797
2999
  res.status(500).json({ error: "Internal Server Error" });
3798
3000
  }
3799
3001
  };
3800
3002
  }
3801
-
3802
3003
  module.exports = { validatePayload };
3803
3004
  ```
3804
3005
 
3805
3006
  ---
3806
3007
 
3807
- This documentation provides a comprehensive guide to using the data
3808
-
3809
- validation module effectively. Ensure to define your validation rules
3810
-
3811
- clearly to maintain data quality and consistency in your applications.
3008
+ This documentation provides a comprehensive guide to using the data validation module effectively. Ensure to define your validation rules clearly to maintain data quality and consistency in your applications.