perfect-payload 1.5.0-beta.1 → 1.7.0-beta.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.
Files changed (3) hide show
  1. package/README.md +1295 -452
  2. package/index.js +247 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,22 +1,48 @@
1
1
  # perfect-payload
2
2
 
3
- A lightweight JavaScript payload validation utility for validating API
4
- and JSON payloads with simple rule-based configuration.
5
-
6
- `perfect-payload` supports structured validation errors, nested field
7
- paths, synchronous custom validators, synchronous payload
8
- transformation/sanitization, array size constraints, and deeply nested
9
- array/object validation while keeping the validation schema simple.
3
+ A lightweight JavaScript payload validation and transformation utility
4
+ for API and JSON payloads.
5
+
6
+ `perfect-payload` provides structured validation errors, exact nested
7
+ field paths, synchronous and asynchronous custom validation, synchronous
8
+ transformation/sanitization, array constraints, and deeply nested
9
+ object/array validation while keeping schemas simple and the package
10
+ lightweight.
11
+
12
+ ## Highlights
13
+
14
+ - Lightweight, rule-based payload validation
15
+ - Structured errors with stable machine-readable codes
16
+ - Exact nested paths such as `profile.email` and
17
+ `products[1].quantity`
18
+ - Recursive `objectAttr` and `elementConstraints` validation
19
+ - `minItems` and `maxItems` array constraints
20
+ - Built-in `trim`, `lowercase`, and `uppercase` sanitization
21
+ - Custom synchronous `transform(value, payload)`
22
+ - Custom synchronous validators with `perfectPayload()`
23
+ - Custom synchronous or asynchronous validators with
24
+ `perfectPayloadAsync()`
25
+ - Transformed values returned through `validatedPayload`
26
+ - Original input payload is not mutated
27
+ - Configurable unknown-field handling: `strip`, `allow`, or `reject`
28
+ - Clean three-argument API with validation options in one object
29
+ - Legacy `perfectPayloadV1()` retained during the migration period
10
30
 
11
31
  ## Quick Links
12
32
 
13
33
  - [Installation](#installation)
14
34
  - [Basic Usage](#basic-usage)
35
+ - [Public API](#public-api)
36
+ - [Unknown Field Handling](#unknown-field-handling)
37
+ - [Synchronous vs Asynchronous
38
+ Validation](#synchronous-vs-asynchronous-validation)
15
39
  - [Validation Rules](#validation-rules)
16
- - [Array Size and Nested Validation](#array-size-and-nested-validation)
40
+ - [Array Size and Nested
41
+ Validation](#array-size-and-nested-validation)
17
42
  - [Transformations and
18
43
  Sanitization](#transformations-and-sanitization)
19
44
  - [Custom Validators](#customvalidator)
45
+ - [Asynchronous Validation](#asynchronous-validation)
20
46
  - [Error Codes](#error-codes)
21
47
  - [Custom Error Messages](#custom-error-messages)
22
48
  - [Nested Objects and Array Field
@@ -24,6 +50,31 @@ array/object validation while keeping the validation schema simple.
24
50
  - [Examples and Usage](#examples-and-usage)
25
51
  - [Legacy API](#legacy-api)
26
52
 
53
+ ## What's New in v1.7.0
54
+
55
+ v1.7.0 introduces two API-level improvements:
56
+
57
+ 1. `unknownFields` gives explicit control over fields that are not
58
+ declared in the validation schema: `"strip"`, `"allow"`, or
59
+ `"reject"`.
60
+ 2. `perfectPayload()` and `perfectPayloadAsync()` now use a clean
61
+ three-argument API where custom response objects and other API
62
+ options live inside one `options` object.
63
+
64
+ ```js
65
+ const result = perfectPayload(payload, rules, {
66
+ unknownFields: "reject",
67
+ inValidPayloadResponse: {
68
+ statusCode: 422,
69
+ valid: false,
70
+ message: "Payload validation failed",
71
+ },
72
+ });
73
+ ```
74
+
75
+ The default `unknownFields` mode is `"strip"`, preserving the previous
76
+ validated-payload filtering behavior when no option is supplied.
77
+
27
78
  ## Installation
28
79
 
29
80
  ```bash
@@ -79,29 +130,28 @@ console.log(result);
79
130
 
80
131
  {
81
132
 
82
-   statusCode: 200,
133
+ statusCode: 200,
83
134
 
84
-   valid: true,
135
+ valid: true,
85
136
 
86
-   validatedPayload: {
137
+ validatedPayload: {
87
138
 
88
-     name: "Kiran",
139
+ name: "Kiran",
89
140
 
90
-     email: "kiran@example.com",
141
+ email: "kiran@example.com",
91
142
 
92
-     age: 29
143
+ age: 29
93
144
 
94
-   }
145
+ }
95
146
 
96
147
  }
97
148
  ```
98
149
 
99
- Note: The validatedPayload contains only the fields
100
- defined in the
101
-
102
- schema, automatically filtering out any extra attributes. You can use it
150
+ By default, `validatedPayload` contains only fields defined in the
151
+ validation schema. Extra payload fields are stripped unless
152
+ `unknownFields` is explicitly configured as `"allow"` or `"reject"`.
103
153
 
104
- to safely overwrite request.body or assign it to a new request property
154
+ The original input payload is not mutated.
105
155
 
106
156
  (such as validatedBody, sanitisedData or parsedBody).
107
157
 
@@ -111,25 +161,25 @@ to safely overwrite request.body or assign it to a new request property
111
161
 
112
162
  {
113
163
 
114
-   statusCode: 400,
164
+ statusCode: 400,
115
165
 
116
-   valid: false,
166
+ valid: false,
117
167
 
118
-   message: "One or more attribute values are invalid",
168
+ message: "One or more attribute values are invalid",
119
169
 
120
-   errors: [
170
+ errors: [
121
171
 
122
-     {
172
+ {
123
173
 
124
-       path: "email",
174
+ path: "email",
125
175
 
126
-       code: "INVALID_EMAIL",
176
+ code: "INVALID_EMAIL",
127
177
 
128
-       message: "Invalid email format for attribute email"
178
+ message: "Invalid email format for attribute email"
129
179
 
130
-     }
180
+ }
131
181
 
132
-   ]
182
+ ]
133
183
 
134
184
  }
135
185
  ```
@@ -140,11 +190,11 @@ Each error returned by `perfectPayload()` contains:
140
190
 
141
191
  {
142
192
 
143
-   path: "field.path",
193
+ path: "field.path",
144
194
 
145
-   code: "ERROR_CODE",
195
+ code: "ERROR_CODE",
146
196
 
147
-   message: "Human readable validation message"
197
+ message: "Human readable validation message"
148
198
 
149
199
  }
150
200
  ```
@@ -159,6 +209,424 @@ failure.
159
209
 
160
210
  \- Submitted payload values are not included in default error messages.
161
211
 
212
+ ## Public API
213
+
214
+ For new implementations, both supported APIs use the same clean
215
+ three-argument signature:
216
+
217
+ ```js
218
+ perfectPayload(data, validationRules, options?)
219
+ await perfectPayloadAsync(data, validationRules, options?)
220
+ ```
221
+
222
+ The arguments are:
223
+
224
+ ---
225
+
226
+ Argument Required Description
227
+
228
+ ---
229
+
230
+ `data` No Payload/object to
231
+ validate. Defaults to
232
+ `{}`.
233
+
234
+ `validationRules` No Validation schema.
235
+ Defaults to `{}`.
236
+
237
+ `options` No API-level configuration
238
+ such as unknown-field
239
+ handling and custom
240
+ response objects.
241
+
242
+ ---
243
+
244
+ The third argument is a single options object. You no longer need to
245
+ pass separate positional arguments for custom valid and invalid
246
+ responses.
247
+
248
+ ### Options
249
+
250
+ ```js
251
+ {
252
+ unknownFields: "strip" | "allow" | "reject",
253
+
254
+ validPayloadResponse: {
255
+ statusCode: 200,
256
+ valid: true,
257
+ },
258
+
259
+ inValidPayloadResponse: {
260
+ statusCode: 400,
261
+ valid: false,
262
+ message: "One or more attribute values are invalid",
263
+ },
264
+ }
265
+ ```
266
+
267
+ All properties are optional.
268
+
269
+ The defaults are equivalent to:
270
+
271
+ ```js
272
+ {
273
+ unknownFields: "strip",
274
+
275
+ validPayloadResponse: {
276
+ statusCode: 200,
277
+ valid: true,
278
+ },
279
+
280
+ inValidPayloadResponse: {
281
+ statusCode: 400,
282
+ valid: false,
283
+ message: "One or more attribute values are invalid",
284
+ },
285
+ }
286
+ ```
287
+
288
+ Example:
289
+
290
+ ```js
291
+ const result = perfectPayload(payload, validationRules, {
292
+ unknownFields: "reject",
293
+
294
+ validPayloadResponse: {
295
+ statusCode: 201,
296
+ valid: true,
297
+ message: "Payload accepted",
298
+ },
299
+
300
+ inValidPayloadResponse: {
301
+ statusCode: 422,
302
+ valid: false,
303
+ message: "Payload validation failed",
304
+ },
305
+ });
306
+ ```
307
+
308
+ The same options object is supported by `perfectPayloadAsync()`:
309
+
310
+ ```js
311
+ const result = await perfectPayloadAsync(payload, validationRules, {
312
+ unknownFields: "reject",
313
+ inValidPayloadResponse: {
314
+ statusCode: 422,
315
+ valid: false,
316
+ message: "Payload validation failed",
317
+ },
318
+ });
319
+ ```
320
+
321
+ ## Unknown Field Handling
322
+
323
+ `unknownFields` controls what happens when the input payload contains a
324
+ field that is not defined in the validation schema.
325
+
326
+ Supported values:
327
+
328
+ ---
329
+
330
+ Value Behavior
331
+
332
+ ---
333
+
334
+ `"strip"` Removes unknown fields from
335
+ `validatedPayload`. This is the
336
+ default and preserves the existing
337
+ behavior.
338
+
339
+ `"allow"` Preserves unknown fields in
340
+ `validatedPayload`.
341
+
342
+ `"reject"` Rejects unknown fields with
343
+ structured `UNKNOWN_FIELD`
344
+ validation errors.
345
+
346
+ ---
347
+
348
+ ### `strip` --- default
349
+
350
+ ```js
351
+ const payload = {
352
+ name: "Kiran",
353
+ role: "developer",
354
+ };
355
+
356
+ const rules = {
357
+ name: {
358
+ type: "string",
359
+ },
360
+ };
361
+
362
+ const result = perfectPayload(payload, rules);
363
+ ```
364
+
365
+ Result:
366
+
367
+ ```js
368
+ {
369
+ statusCode: 200,
370
+ valid: true,
371
+ validatedPayload: {
372
+ name: "Kiran"
373
+ }
374
+ }
375
+ ```
376
+
377
+ `role` is not part of the schema, so it is removed from
378
+ `validatedPayload`.
379
+
380
+ You can also set the default behavior explicitly:
381
+
382
+ ```js
383
+ perfectPayload(payload, rules, {
384
+ unknownFields: "strip",
385
+ });
386
+ ```
387
+
388
+ ### `allow` --- preserve unknown fields
389
+
390
+ ```js
391
+ const result = perfectPayload(payload, rules, {
392
+ unknownFields: "allow",
393
+ });
394
+ ```
395
+
396
+ Result:
397
+
398
+ ```js
399
+ {
400
+ statusCode: 200,
401
+ valid: true,
402
+ validatedPayload: {
403
+ name: "Kiran",
404
+ role: "developer"
405
+ }
406
+ }
407
+ ```
408
+
409
+ Schema-defined fields are still validated normally. Unknown fields are
410
+ simply preserved.
411
+
412
+ ### `reject` --- reject unknown fields
413
+
414
+ ```js
415
+ const result = perfectPayload(payload, rules, {
416
+ unknownFields: "reject",
417
+ });
418
+ ```
419
+
420
+ Result:
421
+
422
+ ```js
423
+ {
424
+ statusCode: 400,
425
+ valid: false,
426
+ message: "One or more attribute values are invalid",
427
+ errors: [
428
+ {
429
+ path: "role",
430
+ code: "UNKNOWN_FIELD",
431
+ message: "Unknown field role is not allowed"
432
+ }
433
+ ]
434
+ }
435
+ ```
436
+
437
+ Unknown-field errors use the same structured error format as all other
438
+ validation errors.
439
+
440
+ ### Nested objects
441
+
442
+ Unknown-field handling is recursive for schemas using `objectAttr`.
443
+
444
+ ```js
445
+ const payload = {
446
+ profile: {
447
+ city: "Bengaluru",
448
+ role: "developer",
449
+ },
450
+ };
451
+
452
+ const rules = {
453
+ profile: {
454
+ type: "object",
455
+ objectAttr: {
456
+ city: {
457
+ type: "string",
458
+ },
459
+ },
460
+ },
461
+ };
462
+
463
+ const result = perfectPayload(payload, rules, {
464
+ unknownFields: "reject",
465
+ });
466
+ ```
467
+
468
+ Returns:
469
+
470
+ ```js
471
+ {
472
+ statusCode: 400,
473
+ valid: false,
474
+ message: "One or more attribute values are invalid",
475
+ errors: [
476
+ {
477
+ path: "profile.role",
478
+ code: "UNKNOWN_FIELD",
479
+ message: "Unknown field profile.role is not allowed"
480
+ }
481
+ ]
482
+ }
483
+ ```
484
+
485
+ ### Arrays and deep paths
486
+
487
+ `unknownFields` also applies recursively through `elementConstraints`.
488
+
489
+ For an unknown field inside an array element, the error path includes
490
+ the array index:
491
+
492
+ ```js
493
+ {
494
+ path: "products[0].internalId",
495
+ code: "UNKNOWN_FIELD",
496
+ message: "Unknown field products[0].internalId is not allowed"
497
+ }
498
+ ```
499
+
500
+ This continues through deeply nested combinations of objects and arrays,
501
+ for example:
502
+
503
+ ```text
504
+ profile.teams[0].members[0].role
505
+ ```
506
+
507
+ ### Normal validation errors and unknown fields
508
+
509
+ With `"reject"`, unknown-field errors can be returned together with
510
+ normal validation errors.
511
+
512
+ For example, an invalid email plus two unknown fields can produce:
513
+
514
+ ```js
515
+ {
516
+ statusCode: 400,
517
+ valid: false,
518
+ message: "One or more attribute values are invalid",
519
+ errors: [
520
+ {
521
+ path: "email",
522
+ code: "INVALID_EMAIL",
523
+ message: "Invalid email format for attribute email"
524
+ },
525
+ {
526
+ path: "role",
527
+ code: "UNKNOWN_FIELD",
528
+ message: "Unknown field role is not allowed"
529
+ },
530
+ {
531
+ path: "active",
532
+ code: "UNKNOWN_FIELD",
533
+ message: "Unknown field active is not allowed"
534
+ }
535
+ ]
536
+ }
537
+ ```
538
+
539
+ With `"allow"`, unknown fields do not create validation errors. Normal
540
+ schema validation continues unchanged.
541
+
542
+ ### Async behavior
543
+
544
+ `perfectPayloadAsync()` supports the same `unknownFields` option:
545
+
546
+ ```js
547
+ const result = await perfectPayloadAsync(payload, rules, {
548
+ unknownFields: "reject",
549
+ });
550
+ ```
551
+
552
+ Unknown-field checking is part of the synchronous validation phase. If
553
+ `"reject"` finds an unknown field, asynchronous `customValidator`
554
+ functions are not executed for that payload. This follows the normal
555
+ two-phase contract of `perfectPayloadAsync()`.
556
+
557
+ ### Own properties only
558
+
559
+ Unknown-field handling considers only the payload object's own
560
+ enumerable properties. Enumerable properties inherited through the
561
+ prototype chain are ignored.
562
+
563
+ ### Invalid option values
564
+
565
+ Only these values are accepted:
566
+
567
+ ```text
568
+ strip
569
+ allow
570
+ reject
571
+ ```
572
+
573
+ Any other value throws a configuration error:
574
+
575
+ ```text
576
+ perfect-payload:- unknownFields must be one of strip, allow, reject
577
+ ```
578
+
579
+ This is a configuration error, not a payload validation error.
580
+
581
+ ## Synchronous vs Asynchronous Validation
582
+
583
+ For normal synchronous validation, use `perfectPayload()`:
584
+
585
+ ```js
586
+ import { perfectPayload } from "perfect-payload";
587
+
588
+ const result = perfectPayload(payload, validationRules, options);
589
+ ```
590
+
591
+ When any `customValidator` needs to perform asynchronous work, use
592
+ `perfectPayloadAsync()` and `await` the result:
593
+
594
+ ```js
595
+ import { perfectPayloadAsync } from "perfect-payload";
596
+
597
+ const result = await perfectPayloadAsync(payload, validationRules, options);
598
+ ```
599
+
600
+ The public APIs are:
601
+
602
+ ```text
603
+ perfectPayloadV1() legacy API; deprecated
604
+ perfectPayload(data, rules, options?) synchronous validation
605
+ perfectPayloadAsync(data, rules, options?) synchronous + asynchronous customValidator
606
+ ```
607
+
608
+ `perfectPayload()` remains synchronous and intentionally rejects a
609
+ `customValidator` that returns a Promise. This preserves the existing
610
+ synchronous API contract.
611
+
612
+ `perfectPayloadAsync()` first performs transformations and normal
613
+ synchronous validation. If synchronous validation fails, the result is
614
+ returned immediately and asynchronous validators are not executed. This
615
+ avoids unnecessary asynchronous work for payloads that are already
616
+ invalid.
617
+
618
+ ```text
619
+ transformations
620
+
621
+ synchronous validation
622
+
623
+ sync errors? ── yes ──→ return validation errors
624
+ ↓ no
625
+ async customValidator
626
+
627
+ return result
628
+ ```
629
+
162
630
  ## Legacy API
163
631
 
164
632
  `perfectPayloadV1()` is still available for backward compatibility.
@@ -173,10 +641,16 @@ March 31, 2027.
173
641
 
174
642
  Existing applications can continue using it during the migration period,
175
643
 
176
- but all new implementations should use:
644
+ but all new implementations should use the current API:
177
645
 
178
646
  ```js
179
- perfectPayload();
647
+ perfectPayload(data, validationRules, options?);
648
+ ```
649
+
650
+ For asynchronous custom validation:
651
+
652
+ ```js
653
+ await perfectPayloadAsync(data, validationRules, options?);
180
654
  ```
181
655
 
182
656
  The legacy API continues to return validation errors as:
@@ -200,6 +674,7 @@ errors: [
200
674
  ```
201
675
 
202
676
  Note: If an inValidPayloadResponse is provided, the
677
+
203
678
  system returns
204
679
 
205
680
  it alongside an automatically generated errors property. Do not include
@@ -211,6 +686,7 @@ object.
211
686
  ## Validation Rules
212
687
 
213
688
  `perfectPayload()` supports validation, nested-schema,
689
+
214
690
  custom-validation, and transformation rules.
215
691
 
216
692
  ### `mandatory`
@@ -305,6 +781,7 @@ Default: Not applied when omitted.
305
781
  const rules = {
306
782
  tags: {
307
783
  type: "array",
784
+
308
785
  minItems: 2,
309
786
  },
310
787
  };
@@ -313,14 +790,20 @@ const rules = {
313
790
  An array with fewer than 2 items returns `MIN_ITEMS`.
314
791
 
315
792
  ```js
793
+
316
794
  {
795
+
317
796
  path: "tags",
797
+
318
798
  code: "MIN_ITEMS",
799
+
319
800
  message: "Attribute tags must contain at least 2 item(s)"
801
+
320
802
  }
321
803
  ```
322
804
 
323
- `minItems` is enforced even when `allowEmptyArray: true` is set. For example, `minItems: 2` still rejects `[]`.
805
+ `minItems` is enforced even when `allowEmptyArray: true` is set. For
806
+ example, `minItems: 2` still rejects `[]`.
324
807
 
325
808
  Error code: `MIN_ITEMS`
326
809
 
@@ -336,6 +819,7 @@ Default: Not applied when omitted.
336
819
  const rules = {
337
820
  tags: {
338
821
  type: "array",
822
+
339
823
  maxItems: 5,
340
824
  },
341
825
  };
@@ -344,10 +828,15 @@ const rules = {
344
828
  An array with more than 5 items returns `MAX_ITEMS`.
345
829
 
346
830
  ```js
831
+
347
832
  {
833
+
348
834
  path: "tags",
835
+
349
836
  code: "MAX_ITEMS",
837
+
350
838
  message: "Attribute tags must contain at most 5 item(s)"
839
+
351
840
  }
352
841
  ```
353
842
 
@@ -554,6 +1043,7 @@ Error code: `MAX_LENGTH`
554
1043
  Prevents decimal numbers.
555
1044
 
556
1045
  Default: `false`; both integer and decimal numbers are
1046
+
557
1047
  allowed.
558
1048
 
559
1049
  Example:
@@ -664,13 +1154,13 @@ Example error:
664
1154
 
665
1155
  {
666
1156
 
667
-   path: "marks[2]",
1157
+ path: "marks[2]",
668
1158
 
669
-   code: "OUT_OF_RANGE",
1159
+ code: "OUT_OF_RANGE",
670
1160
 
671
-   message:
1161
+ message:
672
1162
 
673
-     "Attribute marks[2] should have a value between 0 and 100"
1163
+ "Attribute marks[2] should have a value between 0 and 100"
674
1164
 
675
1165
  }
676
1166
  ```
@@ -739,13 +1229,13 @@ Nested errors include the complete field path:
739
1229
 
740
1230
  {
741
1231
 
742
-   path: "address.location.latitude",
1232
+ path: "address.location.latitude",
743
1233
 
744
-   code: "INVALID_TYPE",
1234
+ code: "INVALID_TYPE",
745
1235
 
746
-   message:
1236
+ message:
747
1237
 
748
-     "Invalid type for attribute address.location.latitude, required number value"
1238
+ "Invalid type for attribute address.location.latitude, required number value"
749
1239
 
750
1240
  }
751
1241
  ```
@@ -784,13 +1274,13 @@ Example error:
784
1274
 
785
1275
  {
786
1276
 
787
-   path: "maxSalary",
1277
+ path: "maxSalary",
788
1278
 
789
-   code: "MIN_VALUE",
1279
+ code: "MIN_VALUE",
790
1280
 
791
-   message:
1281
+ message:
792
1282
 
793
-     "maxSalary must be more than minSalary"
1283
+ "maxSalary must be more than minSalary"
794
1284
 
795
1285
  }
796
1286
  ```
@@ -799,7 +1289,9 @@ Example error:
799
1289
 
800
1290
  ## Array Size and Nested Validation
801
1291
 
802
- `perfectPayload()` supports array size constraints and recursive validation of arrays and objects at multiple depths. Array indexes and nested object keys are preserved in structured error paths.
1292
+ `perfectPayload()` supports array size constraints and recursive
1293
+ validation of arrays and objects at multiple depths. Array indexes and
1294
+ nested object keys are preserved in structured error paths.
803
1295
 
804
1296
  ### Array size constraints
805
1297
 
@@ -809,12 +1301,17 @@ Use `minItems` and `maxItems` with `type: "array"`:
809
1301
  const rules = {
810
1302
  products: {
811
1303
  type: "array",
1304
+
812
1305
  minItems: 1,
1306
+
813
1307
  maxItems: 3,
1308
+
814
1309
  elementConstraints: {
815
1310
  type: "object",
1311
+
816
1312
  objectAttr: {
817
1313
  productId: { mandatory: true, type: "string" },
1314
+
818
1315
  quantity: { mandatory: true, type: "number", min: 1 },
819
1316
  },
820
1317
  },
@@ -825,18 +1322,26 @@ const rules = {
825
1322
  If the array is empty, `minItems` reports the array path itself:
826
1323
 
827
1324
  ```js
1325
+
828
1326
  {
1327
+
829
1328
  path: "products",
1329
+
830
1330
  code: "MIN_ITEMS",
1331
+
831
1332
  message: "Attribute products must contain at least 1 item(s)"
1333
+
832
1334
  }
833
1335
  ```
834
1336
 
835
1337
  ### Arrays of objects
836
1338
 
837
- `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:
1339
+ `elementConstraints` can contain `objectAttr`, allowing every object in
1340
+ an array to use a nested schema. An invalid quantity in the second
1341
+ product is reported as:
838
1342
 
839
1343
  ```text
1344
+
840
1345
  products[1].quantity
841
1346
  ```
842
1347
 
@@ -848,21 +1353,32 @@ products[1].quantity
848
1353
  const rules = {
849
1354
  orders: {
850
1355
  type: "array",
1356
+
851
1357
  minItems: 1,
1358
+
852
1359
  maxItems: 2,
1360
+
853
1361
  elementConstraints: {
854
1362
  type: "object",
1363
+
855
1364
  objectAttr: {
856
1365
  orderId: { mandatory: true, type: "string" },
1366
+
857
1367
  items: {
858
1368
  mandatory: true,
1369
+
859
1370
  type: "array",
1371
+
860
1372
  minItems: 1,
1373
+
861
1374
  maxItems: 2,
1375
+
862
1376
  elementConstraints: {
863
1377
  type: "object",
1378
+
864
1379
  objectAttr: {
865
1380
  productId: { mandatory: true, type: "string" },
1381
+
866
1382
  quantity: { mandatory: true, type: "number", min: 1 },
867
1383
  },
868
1384
  },
@@ -873,31 +1389,41 @@ const rules = {
873
1389
  };
874
1390
  ```
875
1391
 
876
- A deep validation failure preserves the complete indexed path, for example:
1392
+ A deep validation failure preserves the complete indexed path, for
1393
+ example:
877
1394
 
878
1395
  ```text
1396
+
879
1397
  orders[1].items[2].quantity
880
1398
  ```
881
1399
 
882
- Array constraints work at nested levels too. A nested array can report paths such as:
1400
+ Array constraints work at nested levels too. A nested array can report
1401
+ paths such as:
883
1402
 
884
1403
  ```text
1404
+
885
1405
  orders[1].items
886
1406
  ```
887
1407
 
888
1408
  Nested arrays are supported and every array index is preserved:
889
1409
 
890
1410
  ```text
1411
+
891
1412
  matrix[1][1]
1413
+
892
1414
  matrix[1][1][1]
893
1415
  ```
894
1416
 
895
- Transformations applied inside nested objects or array elements are preserved in `validatedPayload`, while the original input remains unchanged.
1417
+ Transformations applied inside nested objects or array elements are
1418
+ preserved in `validatedPayload`, while the original input remains
1419
+ unchanged.
896
1420
 
897
1421
  ### Transformations and Sanitization
898
1422
 
899
1423
  `perfectPayload()` can transform a field before its validation rules
1424
+
900
1425
  run. The transformed value is returned in `validatedPayload`, while the
1426
+
901
1427
  original input object is not mutated.
902
1428
 
903
1429
  Supported transformation rules:
@@ -907,53 +1433,81 @@ Rule Purpose
907
1433
  ---
908
1434
 
909
1435
  `trim` Removes leading and trailing whitespace from strings
1436
+
910
1437
  `lowercase` Converts strings to lowercase
1438
+
911
1439
  `uppercase` Converts strings to uppercase
1440
+
912
1441
  `transform` Runs a custom synchronous transformation function
913
1442
 
914
1443
  Transformations always run in this fixed order, regardless of the order
1444
+
915
1445
  in which the rule properties are written:
916
1446
 
917
1447
  ```text
1448
+
918
1449
  trim
1450
+
919
1451
 
1452
+
920
1453
  lowercase
1454
+
921
1455
 
1456
+
922
1457
  uppercase
1458
+
923
1459
 
1460
+
924
1461
  transform(value, payload)
1462
+
925
1463
 
1464
+
926
1465
  validation rules
1466
+
927
1467
 
1468
+
928
1469
  customValidator
1470
+
929
1471
 
1472
+
930
1473
  validatedPayload
931
1474
  ```
932
1475
 
933
1476
  #### `trim`
934
1477
 
935
1478
  ```js
1479
+
936
1480
  const payload = {
1481
+
937
1482
  name: " Kiran Poojary ",
1483
+
938
1484
  };
939
1485
 
940
1486
  const rules = {
1487
+
941
1488
  name: {
1489
+
942
1490
  type: "string",
1491
+
943
1492
  trim: true,
1493
+
944
1494
  },
1495
+
945
1496
  };
946
1497
 
947
1498
  const result = perfectPayload(payload, rules);
948
1499
 
949
1500
  console.log(result.validatedPayload.name);
950
- // "Kiran Poojary"
1501
+
1502
+ *// "Kiran Poojary"*
951
1503
 
952
1504
  console.log(payload.name);
953
- // " Kiran Poojary "
1505
+
1506
+ *// " Kiran Poojary "*
954
1507
  ```
955
1508
 
956
1509
  `trim` applies only to string values. Non-string values are left
1510
+
957
1511
  unchanged.
958
1512
 
959
1513
  #### `lowercase`
@@ -962,13 +1516,16 @@ unchanged.
962
1516
  const rules = {
963
1517
  email: {
964
1518
  trim: true,
1519
+
965
1520
  lowercase: true,
1521
+
966
1522
  type: "email",
967
1523
  },
968
1524
  };
969
1525
  ```
970
1526
 
971
1527
  For `" KIRAN@EXAMPLE.COM "`, the validated value becomes
1528
+
972
1529
  `"kiran@example.com"`.
973
1530
 
974
1531
  #### `uppercase`
@@ -977,6 +1534,7 @@ For `" KIRAN@EXAMPLE.COM "`, the validated value becomes
977
1534
  const rules = {
978
1535
  countryCode: {
979
1536
  type: "string",
1537
+
980
1538
  uppercase: true,
981
1539
  },
982
1540
  };
@@ -985,6 +1543,7 @@ const rules = {
985
1543
  For `"in"`, the validated value becomes `"IN"`.
986
1544
 
987
1545
  `lowercase: true` and `uppercase: true` cannot be enabled together for
1546
+
988
1547
  the same field. Doing so throws a schema configuration error.
989
1548
 
990
1549
  #### `transform`
@@ -995,6 +1554,7 @@ Use `transform` when the built-in string transformations are not enough.
995
1554
  const rules = {
996
1555
  phone: {
997
1556
  type: "string",
1557
+
998
1558
  transform: (value) => value.replace(/\s+/g, ""),
999
1559
  },
1000
1560
  };
@@ -1011,31 +1571,46 @@ transform: (value, payload) => {
1011
1571
  ```
1012
1572
 
1013
1573
  - `value` is the field value after the built-in transformations have
1574
+
1014
1575
  run.
1576
+
1015
1577
  - `payload` is the current payload/object being validated.
1016
1578
 
1017
1579
  This makes cross-field transformations possible:
1018
1580
 
1019
1581
  ```js
1582
+
1020
1583
  const payload = {
1584
+
1021
1585
  amount: 100,
1586
+
1022
1587
  multiplier: 2,
1588
+
1023
1589
  };
1024
1590
 
1025
1591
  const rules = {
1592
+
1026
1593
  amount: {
1594
+
1027
1595
  transform: (value, payload) => value * payload.multiplier,
1596
+
1028
1597
  type: "number",
1598
+
1029
1599
  },
1600
+
1030
1601
  multiplier: {
1602
+
1031
1603
  type: "number",
1604
+
1032
1605
  },
1606
+
1033
1607
  };
1034
1608
 
1035
1609
  const result = perfectPayload(payload, rules);
1036
1610
 
1037
1611
  console.log(result.validatedPayload.amount);
1038
- // 200
1612
+
1613
+ *// 200*
1039
1614
  ```
1040
1615
 
1041
1616
  A custom transformer may also change the data type before validation:
@@ -1044,36 +1619,48 @@ A custom transformer may also change the data type before validation:
1044
1619
  const rules = {
1045
1620
  quantity: {
1046
1621
  transform: (value) => Number(value),
1622
+
1047
1623
  type: "number",
1624
+
1048
1625
  min: 1,
1626
+
1049
1627
  max: 100,
1050
1628
  },
1051
1629
  };
1052
1630
  ```
1053
1631
 
1054
1632
  The transformed value is validated by the normal validation rules and is
1633
+
1055
1634
  also the value received by `customValidator`.
1056
1635
 
1057
1636
  Transformations work inside `objectAttr` and `elementConstraints`, and
1637
+
1058
1638
  transformed nested/array values are preserved in `validatedPayload`.
1059
1639
 
1060
1640
  ```js
1061
1641
  const rules = {
1062
1642
  profile: {
1063
1643
  type: "object",
1644
+
1064
1645
  objectAttr: {
1065
1646
  name: {
1066
1647
  trim: true,
1648
+
1067
1649
  uppercase: true,
1650
+
1068
1651
  type: "string",
1069
1652
  },
1070
1653
  },
1071
1654
  },
1655
+
1072
1656
  tags: {
1073
1657
  type: "array",
1658
+
1074
1659
  elementConstraints: {
1075
1660
  trim: true,
1661
+
1076
1662
  lowercase: true,
1663
+
1077
1664
  type: "string",
1078
1665
  },
1079
1666
  },
@@ -1081,29 +1668,39 @@ const rules = {
1081
1668
  ```
1082
1669
 
1083
1670
  Missing optional fields are not transformed. An input value of `null` is
1671
+
1084
1672
  not passed to transformation functions; null handling remains controlled
1673
+
1085
1674
  by `allowNull`.
1086
1675
 
1087
- **Important:** `transform` is synchronous. A non-function transformer,
1676
+ \*\*\*\*Important:\*\*\*\* `transform` is synchronous. A non-function
1677
+ transformer,
1678
+
1088
1679
  an `async` transformer, a transformer that returns a Promise, or a
1089
- transformer that returns `undefined` is not supported and throws an error.
1090
- Returning `null`, `""`, `0`, or `false` is allowed; the transformed value is
1091
- then processed by the normal validation rules. Exceptions thrown inside the
1680
+
1681
+ transformer that returns `undefined` is not supported and throws an
1682
+ error.
1683
+
1684
+ Returning `null`, `""`, `0`, or `false` is allowed; the transformed
1685
+ value is
1686
+
1687
+ then processed by the normal validation rules. Exceptions thrown inside
1688
+ the
1689
+
1092
1690
  transformer propagate to the caller.
1093
1691
 
1094
1692
  For example, returning `undefined` throws:
1095
1693
 
1096
1694
  ```text
1695
+
1097
1696
  perfect-payload:- transform must not return undefined for attribute username
1098
1697
  ```
1099
1698
 
1100
1699
  ### `customValidator`
1101
1700
 
1102
- Allows you to define custom synchronous validation logic for a field
1103
- when the built-in validation rules are not enough.
1701
+ Defines custom validation logic when the built-in rules are not enough.
1104
1702
 
1105
- The validator receives the field value and the current payload/object
1106
- being validated:
1703
+ The validator receives:
1107
1704
 
1108
1705
  ```js
1109
1706
  customValidator: (value, payload) => {
@@ -1111,100 +1708,302 @@ customValidator: (value, payload) => {
1111
1708
  };
1112
1709
  ```
1113
1710
 
1114
- The validator must return `true` to pass validation. Any other return
1115
- value causes validation to fail.
1711
+ - `value` is the field value after transformations have been applied.
1712
+ - `payload` is the current payload/object being validated.
1713
+ - Return `true` to pass.
1714
+ - Any value other than `true` fails validation.
1715
+ - Exceptions thrown by the validator propagate to the caller.
1716
+
1717
+ For nested validation, `payload` means the current nested object rather
1718
+ than the root request body.
1116
1719
 
1117
- Example:
1720
+ #### Synchronous custom validator
1721
+
1722
+ Use a synchronous validator with `perfectPayload()`:
1118
1723
 
1119
1724
  ```js
1120
1725
  const rules = {
1121
1726
  username: {
1122
1727
  mandatory: true,
1123
-
1124
1728
  type: "string",
1729
+ trim: true,
1125
1730
 
1126
1731
  customValidator: (value) => {
1127
1732
  return !value.toLowerCase().includes("admin");
1128
1733
  },
1129
1734
 
1130
1735
  customValidatorCode: "RESERVED_USERNAME",
1131
-
1132
1736
  customValidatorError: "Username cannot contain admin",
1133
1737
  },
1134
1738
  };
1739
+
1740
+ const result = perfectPayload({ username: " admin_kiran " }, rules);
1135
1741
  ```
1136
1742
 
1137
- For this payload:
1743
+ A failure returns:
1138
1744
 
1139
1745
  ```js
1140
- const payload = {
1141
- username: "admin_kiran",
1746
+ {
1747
+ statusCode: 400,
1748
+ valid: false,
1749
+ message: "One or more attribute values are invalid",
1750
+ errors: [
1751
+ {
1752
+ path: "username",
1753
+ code: "RESERVED_USERNAME",
1754
+ message: "Username cannot contain admin"
1755
+ }
1756
+ ]
1757
+ }
1758
+ ```
1759
+
1760
+ The current payload/object can be used for cross-field validation:
1761
+
1762
+ ```js
1763
+ const rules = {
1764
+ limit: {
1765
+ type: "number",
1766
+ },
1767
+
1768
+ amount: {
1769
+ type: "number",
1770
+
1771
+ customValidator: (value, payload) => {
1772
+ return value <= payload.limit;
1773
+ },
1774
+
1775
+ customValidatorCode: "LIMIT_EXCEEDED",
1776
+ customValidatorError: "Amount cannot exceed limit",
1777
+ },
1142
1778
  };
1143
1779
  ```
1144
1780
 
1145
- The validation error is:
1781
+ If `customValidatorCode` and `customValidatorError` are omitted, the
1782
+ default error is:
1146
1783
 
1147
1784
  ```js
1785
+ {
1786
+ path: "username",
1787
+ code: "CUSTOM_VALIDATION_FAILED",
1788
+ message: "Custom validation failed for attribute username"
1789
+ }
1790
+ ```
1791
+
1792
+ `customValidator` works recursively inside `objectAttr` and
1793
+ `elementConstraints`. Structured errors preserve the corresponding
1794
+ nested and array paths.
1795
+
1796
+ When using `perfectPayload()`, `customValidator` must remain
1797
+ synchronous. A Promise-returning validator throws:
1798
+
1799
+ ```text
1800
+ perfect-payload:- customValidator must be synchronous for attribute username
1801
+ ```
1802
+
1803
+ For asynchronous custom validation, use `perfectPayloadAsync()`.
1804
+
1805
+ ## Asynchronous Validation
1806
+
1807
+ `perfectPayloadAsync()` supports both synchronous and asynchronous
1808
+ `customValidator` functions without changing the behavior of
1809
+ `perfectPayload()`.
1148
1810
 
1811
+ ```js
1812
+ import { perfectPayloadAsync } from "perfect-payload";
1813
+
1814
+ const rules = {
1815
+ username: {
1816
+ mandatory: true,
1817
+ type: "string",
1818
+ trim: true,
1819
+
1820
+ customValidator: async (value) => {
1821
+ const available = await checkUsernameAvailability(value);
1822
+ return available;
1823
+ },
1824
+
1825
+ customValidatorCode: "USERNAME_TAKEN",
1826
+ customValidatorError: "Username is already taken",
1827
+ },
1828
+ };
1829
+
1830
+ const result = await perfectPayloadAsync(
1831
+ {
1832
+ username: " kiran ",
1833
+ },
1834
+ rules,
1835
+ );
1836
+ ```
1837
+
1838
+ On success, transformations are preserved:
1839
+
1840
+ ```js
1149
1841
  {
1842
+ statusCode: 200,
1843
+ valid: true,
1844
+ validatedPayload: {
1845
+ username: "kiran"
1846
+ }
1847
+ }
1848
+ ```
1849
+
1850
+ On asynchronous validation failure:
1851
+
1852
+ ```js
1853
+ {
1854
+ statusCode: 400,
1855
+ valid: false,
1856
+ message: "One or more attribute values are invalid",
1857
+ errors: [
1858
+ {
1859
+ path: "username",
1860
+ code: "USERNAME_TAKEN",
1861
+ message: "Username is already taken"
1862
+ }
1863
+ ]
1864
+ }
1865
+ ```
1866
+
1867
+ ### Async validator contract
1868
+
1869
+ For `perfectPayloadAsync()`:
1870
+
1871
+ ```text
1872
+ true → pass
1873
+ false → validation failure
1874
+ anything != true → validation failure
1875
+ throw → exception propagates
1876
+ rejected Promise → rejection propagates
1877
+ ```
1878
+
1879
+ A normal synchronous validator is also valid when using the asynchronous
1880
+ API:
1881
+
1882
+ ```js
1883
+ const rules = {
1884
+ username: {
1885
+ type: "string",
1886
+ customValidator: (value) => value !== "admin",
1887
+ },
1888
+ };
1889
+
1890
+ const result = await perfectPayloadAsync(payload, rules);
1891
+ ```
1892
+
1893
+ A configured `customValidator` must be a function. Otherwise an error is
1894
+ thrown:
1895
+
1896
+ ```text
1897
+ perfect-payload:- customValidator must be a function for attribute username
1898
+ ```
1899
+
1900
+ ### Validation order
1901
+
1902
+ `perfectPayloadAsync()` uses two phases:
1903
+
1904
+ 1. Transform the payload and run normal synchronous validation.
1905
+ 2. If phase 1 succeeds, run custom validators with `await`.
1906
+
1907
+ If any synchronous validation error exists, phase 2 is skipped and the
1908
+ synchronous validation result is returned immediately.
1909
+
1910
+ This means asynchronous validators can assume the payload has already
1911
+ passed its normal synchronous validation rules.
1912
+
1913
+ ### Nested async validation
1914
+
1915
+ Async custom validators work recursively inside `objectAttr`:
1916
+
1917
+ ```js
1918
+ const rules = {
1919
+ profile: {
1920
+ type: "object",
1921
+
1922
+ objectAttr: {
1923
+ username: {
1924
+ type: "string",
1925
+ trim: true,
1926
+
1927
+ customValidator: async (value) => {
1928
+ return await isUsernameAvailable(value);
1929
+ },
1930
+
1931
+ customValidatorCode: "USERNAME_TAKEN",
1932
+ customValidatorError: "Username is already taken",
1933
+ },
1934
+ },
1935
+ },
1936
+ };
1937
+ ```
1938
+
1939
+ A failure produces the complete path:
1940
+
1941
+ ```text
1942
+ profile.username
1943
+ ```
1944
+
1945
+ They also work inside `elementConstraints`:
1946
+
1947
+ ```js
1948
+ const rules = {
1949
+ usernames: {
1950
+ type: "array",
1951
+
1952
+ elementConstraints: {
1953
+ type: "string",
1954
+ trim: true,
1955
+
1956
+ customValidator: async (value) => {
1957
+ return await isUsernameAvailable(value);
1958
+ },
1959
+
1960
+ customValidatorCode: "USERNAME_TAKEN",
1961
+ customValidatorError: "Username is already taken",
1962
+ },
1963
+ },
1964
+ };
1965
+ ```
1150
1966
 
1151
-   path: "username",
1967
+ For an invalid second element:
1152
1968
 
1153
-   code: "RESERVED_USERNAME",
1969
+ ```text
1970
+ usernames[1]
1971
+ ```
1154
1972
 
1155
-   message: "Username cannot contain admin"
1973
+ Deep combinations of objects and arrays preserve every level of the
1974
+ path:
1156
1975
 
1157
- }
1976
+ ```text
1977
+ products[1].seller.username
1978
+ profile.teams[1].members[1].username
1158
1979
  ```
1159
1980
 
1160
- If `customValidatorCode` and `customValidatorError` are not provided,
1161
- the default error is:
1981
+ Default async custom-validation messages also use the final indexed
1982
+ path:
1162
1983
 
1163
1984
  ```js
1164
-
1165
1985
  {
1166
-
1167
-   path: "username",
1168
-
1169
-   code: "CUSTOM_VALIDATION_FAILED",
1170
-
1171
-   message: "Custom validation failed for attribute username"
1172
-
1986
+ path: "users[1].username",
1987
+ code: "CUSTOM_VALIDATION_FAILED",
1988
+ message: "Custom validation failed for attribute users[1].username"
1173
1989
  }
1174
1990
  ```
1175
1991
 
1176
- The current payload/object being validated can be used as the second
1177
- argument when required:
1178
-
1179
- ```js
1180
- const rules = {
1181
- limit: {
1182
- type: "number",
1183
- },
1184
-
1185
- amount: {
1186
- type: "number",
1992
+ ### Transform remains synchronous
1187
1993
 
1188
- customValidator: (value, payload) => {
1189
- return value <= payload.limit;
1190
- },
1994
+ `perfectPayloadAsync()` makes custom validation asynchronous; it does
1995
+ not make `transform` asynchronous.
1191
1996
 
1192
- customValidatorCode: "LIMIT_EXCEEDED",
1997
+ `transform` must still be synchronous:
1193
1998
 
1194
- customValidatorError: "Amount cannot exceed limit",
1195
- },
1999
+ ```js
2000
+ transform: (value, payload) => {
2001
+ return value;
1196
2002
  };
1197
2003
  ```
1198
2004
 
1199
- `customValidator` also works with nested objects and array
1200
- `elementConstraints`. The generated structured error automatically
1201
- contains the corresponding nested or array path.
1202
-
1203
- Important: `customValidator` is synchronous. An `async`
1204
- validator or a validator that returns a Promise is not supported and
1205
- throws an error. Asynchronous validation is not part of this feature.
1206
-
1207
- Error code when no custom code is provided: `CUSTOM_VALIDATION_FAILED`
2005
+ An async transformer or a transformer that returns a Promise is not
2006
+ supported.
1208
2007
 
1209
2008
  ## Error Codes
1210
2009
 
@@ -1263,6 +2062,8 @@ MAX_VALUE
1263
2062
  OUT_OF_RANGE
1264
2063
 
1265
2064
  CUSTOM_VALIDATION_FAILED
2065
+
2066
+ UNKNOWN_FIELD
1266
2067
  ```
1267
2068
 
1268
2069
  These codes are designed for programmatic handling while `message`
@@ -1277,17 +2078,17 @@ const result = perfectPayload(payload, validationRules);
1277
2078
 
1278
2079
  if (!result.valid) {
1279
2080
 
1280
-   const emailError = result.errors.find(
2081
+ const emailError = result.errors.find(
1281
2082
 
1282
-     (error) => error.code === "INVALID_EMAIL",
2083
+ (error) => error.code === "INVALID_EMAIL",
1283
2084
 
1284
-   );
2085
+ );
1285
2086
 
1286
-   if (emailError) {
2087
+ if (emailError) {
1287
2088
 
1288
-     **// Handle invalid email**
2089
+ ***// Handle invalid email***
1289
2090
 
1290
-   }
2091
+ }
1291
2092
 
1292
2093
  }
1293
2094
  ```
@@ -1304,11 +2105,11 @@ keeping the same structured error format:
1304
2105
 
1305
2106
  {
1306
2107
 
1307
-   path: "email",
2108
+ path: "email",
1308
2109
 
1309
-   code: "INVALID_EMAIL",
2110
+ code: "INVALID_EMAIL",
1310
2111
 
1311
-   message: "Email address is invalid"
2112
+ message: "Email address is invalid"
1312
2113
 
1313
2114
  }
1314
2115
  ```
@@ -1335,11 +2136,11 @@ If `email` is missing:
1335
2136
 
1336
2137
  {
1337
2138
 
1338
-   path: "email",
2139
+ path: "email",
1339
2140
 
1340
-   code: "REQUIRED",
2141
+ code: "REQUIRED",
1341
2142
 
1342
-   message: "Email is required"
2143
+ message: "Email is required"
1343
2144
 
1344
2145
  }
1345
2146
  ```
@@ -1350,11 +2151,11 @@ If `email` is present but invalid:
1350
2151
 
1351
2152
  {
1352
2153
 
1353
-   path: "email",
2154
+ path: "email",
1354
2155
 
1355
-   code: "INVALID_EMAIL",
2156
+ code: "INVALID_EMAIL",
1356
2157
 
1357
-   message: "Email address is invalid"
2158
+ message: "Email address is invalid"
1358
2159
 
1359
2160
  }
1360
2161
  ```
@@ -1443,53 +2244,53 @@ Example result:
1443
2244
 
1444
2245
  {
1445
2246
 
1446
-   statusCode: 400,
2247
+ statusCode: 400,
1447
2248
 
1448
-   valid: false,
2249
+ valid: false,
1449
2250
 
1450
-   message:
2251
+ message:
1451
2252
 
1452
-     "One or more attribute values are invalid",
2253
+ "One or more attribute values are invalid",
1453
2254
 
1454
-   errors: [
2255
+ errors: [
1455
2256
 
1456
-     {
2257
+ {
1457
2258
 
1458
-       path: "username",
2259
+ path: "username",
1459
2260
 
1460
-       code: "MIN_LENGTH",
2261
+ code: "MIN_LENGTH",
1461
2262
 
1462
-       message:
2263
+ message:
1463
2264
 
1464
-         "Username must contain at least 3 characters"
2265
+ "Username must contain at least 3 characters"
1465
2266
 
1466
-     },
2267
+ },
1467
2268
 
1468
-     {
2269
+ {
1469
2270
 
1470
-       path: "age",
2271
+ path: "age",
1471
2272
 
1472
-       code: "MIN_VALUE",
2273
+ code: "MIN_VALUE",
1473
2274
 
1474
-       message:
2275
+ message:
1475
2276
 
1476
-         "Age must be at least 18"
2277
+ "Age must be at least 18"
1477
2278
 
1478
-     },
2279
+ },
1479
2280
 
1480
-     {
2281
+ {
1481
2282
 
1482
-       path: "score",
2283
+ path: "score",
1483
2284
 
1484
-       code: "OUT_OF_RANGE",
2285
+ code: "OUT_OF_RANGE",
1485
2286
 
1486
-       message:
2287
+ message:
1487
2288
 
1488
-         "Score must be between 0 and 100"
2289
+ "Score must be between 0 and 100"
1489
2290
 
1490
-     }
2291
+ }
1491
2292
 
1492
-   ]
2293
+ ]
1493
2294
 
1494
2295
  }
1495
2296
  ```
@@ -1520,11 +2321,11 @@ Still returns:
1520
2321
 
1521
2322
  {
1522
2323
 
1523
-   path: "age",
2324
+ path: "age",
1524
2325
 
1525
-   code: "MIN_VALUE",
2326
+ code: "MIN_VALUE",
1526
2327
 
1527
-   message: "You must be 18 or older"
2328
+ message: "You must be 18 or older"
1528
2329
 
1529
2330
  }
1530
2331
  ```
@@ -1541,157 +2342,156 @@ handling
1541
2342
 
1542
2343
  ## Custom Response Objects
1543
2344
 
1544
- `perfectPayload()` allows you to customize both the valid and invalid
1545
-
1546
- response objects.
2345
+ Custom valid and invalid response objects are configured inside the
2346
+ optional third `options` argument.
1547
2347
 
1548
- The third argument is the custom valid response.
2348
+ ```js
2349
+ perfectPayload(data, validationRules, options?)
2350
+ await perfectPayloadAsync(data, validationRules, options?)
2351
+ ```
1549
2352
 
1550
- The fourth argument is the custom invalid response.
2353
+ This keeps API-level configuration in one place and avoids positional
2354
+ `undefined` arguments.
1551
2355
 
1552
2356
  ### Custom Valid Response
1553
2357
 
1554
- Example:
1555
-
1556
2358
  ```js
1557
- const customValidResponse = {
1558
- statusCode: 201,
1559
-
1560
- valid: true,
1561
-
1562
- message: "Payload validated successfully",
1563
- };
1564
-
1565
- const result = perfectPayload(payload, validationRules, customValidResponse);
2359
+ const result = perfectPayload(payload, validationRules, {
2360
+ validPayloadResponse: {
2361
+ statusCode: 201,
2362
+ valid: true,
2363
+ message: "Payload validated successfully",
2364
+ },
2365
+ });
1566
2366
  ```
1567
2367
 
1568
2368
  When validation succeeds, `validatedPayload` is automatically added:
1569
2369
 
1570
2370
  ```js
1571
-
1572
2371
  {
1573
-
1574
-   statusCode: 201,
1575
-
1576
-   valid: true,
1577
-
1578
-   message: "Payload validated successfully",
1579
-
1580
-   validatedPayload: {
1581
-
1582
-     name: "Kiran",
1583
-
1584
-     email: "kiran@example.com",
1585
-
1586
-     age: 29
1587
-
1588
-   }
1589
-
2372
+ statusCode: 201,
2373
+ valid: true,
2374
+ message: "Payload validated successfully",
2375
+ validatedPayload: {
2376
+ name: "Kiran",
2377
+ email: "kiran@example.com",
2378
+ age: 29
2379
+ }
1590
2380
  }
1591
2381
  ```
1592
2382
 
1593
2383
  ### Custom Invalid Response
1594
2384
 
1595
- Example:
1596
-
1597
2385
  ```js
1598
- const customInvalidResponse = {
1599
- statusCode: 422,
1600
-
1601
- valid: false,
1602
-
1603
- message: "Payload validation failed",
1604
- };
1605
-
1606
- const result = perfectPayload(
1607
- payload,
1608
-
1609
- validationRules,
1610
-
1611
- undefined,
1612
-
1613
- customInvalidResponse,
1614
- );
2386
+ const result = perfectPayload(payload, validationRules, {
2387
+ inValidPayloadResponse: {
2388
+ statusCode: 422,
2389
+ valid: false,
2390
+ message: "Payload validation failed",
2391
+ },
2392
+ });
1615
2393
  ```
1616
2394
 
1617
2395
  When validation fails, `errors` is automatically added:
1618
2396
 
1619
2397
  ```js
1620
-
1621
2398
  {
1622
-
1623
-   statusCode: 422,
1624
-
1625
-   valid: false,
1626
-
1627
-   message: "Payload validation failed",
1628
-
1629
-   errors: [
1630
-
1631
-     {
1632
-
1633
-       path: "email",
1634
-
1635
-       code: "INVALID_EMAIL",
1636
-
1637
-       message:
1638
-
1639
-         "Invalid email format for attribute email"
1640
-
1641
-     }
1642
-
1643
-   ]
1644
-
2399
+ statusCode: 422,
2400
+ valid: false,
2401
+ message: "Payload validation failed",
2402
+ errors: [
2403
+ {
2404
+ path: "email",
2405
+ code: "INVALID_EMAIL",
2406
+ message: "Invalid email format for attribute email"
2407
+ }
2408
+ ]
1645
2409
  }
1646
2410
  ```
1647
2411
 
1648
2412
  ### Custom Valid and Invalid Responses Together
1649
2413
 
1650
2414
  ```js
1651
- const customValidResponse = {
1652
- statusCode: 201,
2415
+ const result = perfectPayload(payload, validationRules, {
2416
+ validPayloadResponse: {
2417
+ statusCode: 201,
2418
+ valid: true,
2419
+ message: "CUSTOM_VALID_RESPONSE",
2420
+ },
1653
2421
 
1654
- valid: true,
2422
+ inValidPayloadResponse: {
2423
+ statusCode: 422,
2424
+ valid: false,
2425
+ message: "CUSTOM_INVALID_RESPONSE",
2426
+ },
2427
+ });
2428
+ ```
1655
2429
 
1656
- message: "CUSTOM_VALID_RESPONSE",
1657
- };
2430
+ You can combine response customization with other API options:
1658
2431
 
1659
- const customInvalidResponse = {
1660
- statusCode: 422,
2432
+ ```js
2433
+ const result = perfectPayload(payload, validationRules, {
2434
+ unknownFields: "reject",
1661
2435
 
1662
- valid: false,
2436
+ validPayloadResponse: {
2437
+ statusCode: 201,
2438
+ valid: true,
2439
+ },
1663
2440
 
1664
- message: "CUSTOM_INVALID_RESPONSE",
1665
- };
2441
+ inValidPayloadResponse: {
2442
+ statusCode: 422,
2443
+ valid: false,
2444
+ message: "Payload validation failed",
2445
+ },
2446
+ });
2447
+ ```
1666
2448
 
1667
- const result = perfectPayload(
1668
- payload,
2449
+ The response object you provide is preserved while `perfectPayload()`
2450
+ automatically adds `validatedPayload` for successful validation or
2451
+ `errors` for failed validation.
1669
2452
 
1670
- validationRules,
2453
+ The same response options are supported by `perfectPayloadAsync()`.
1671
2454
 
1672
- customValidResponse,
2455
+ ## v1.7 API Migration
1673
2456
 
1674
- customInvalidResponse,
1675
- );
1676
- ```
2457
+ The current `perfectPayload()` and `perfectPayloadAsync()` APIs use one
2458
+ optional third argument for configuration:
1677
2459
 
1678
- The response object you provide is preserved, while `perfectPayload()`
2460
+ ```js
2461
+ perfectPayload(data, validationRules, options?)
2462
+ perfectPayloadAsync(data, validationRules, options?)
2463
+ ```
1679
2464
 
1680
- automatically adds either:
2465
+ Custom response objects now belong inside `options`.
1681
2466
 
1682
- ```text
2467
+ Use:
1683
2468
 
1684
- validatedPayload
2469
+ ```js
2470
+ perfectPayload(payload, rules, {
2471
+ validPayloadResponse: customValidResponse,
2472
+ inValidPayloadResponse: customInvalidResponse,
2473
+ });
1685
2474
  ```
1686
2475
 
1687
- for successful validation, or:
2476
+ instead of passing custom response objects as separate positional
2477
+ arguments.
1688
2478
 
1689
- ```text
2479
+ This also makes it possible to combine response customization with
2480
+ `unknownFields` without placeholder arguments:
1690
2481
 
1691
- errors
2482
+ ```js
2483
+ perfectPayload(payload, rules, {
2484
+ unknownFields: "reject",
2485
+ inValidPayloadResponse: {
2486
+ statusCode: 422,
2487
+ valid: false,
2488
+ message: "Payload validation failed",
2489
+ },
2490
+ });
1692
2491
  ```
1693
2492
 
1694
- for failed validation.
2493
+ `perfectPayloadV1()` is unchanged and retains its legacy signature
2494
+ during its deprecation period.
1695
2495
 
1696
2496
  ## Default Responses
1697
2497
 
@@ -1703,15 +2503,15 @@ is:
1703
2503
 
1704
2504
  {
1705
2505
 
1706
-   statusCode: 200,
2506
+ statusCode: 200,
1707
2507
 
1708
-   valid: true,
2508
+ valid: true,
1709
2509
 
1710
-   validatedPayload: {
2510
+ validatedPayload: {
1711
2511
 
1712
-     **// validated fields**
2512
+ ***// validated fields***
1713
2513
 
1714
-   }
2514
+ }
1715
2515
 
1716
2516
  }
1717
2517
  ```
@@ -1722,25 +2522,25 @@ The default invalid response is:
1722
2522
 
1723
2523
  {
1724
2524
 
1725
-   statusCode: 400,
2525
+ statusCode: 400,
1726
2526
 
1727
-   valid: false,
2527
+ valid: false,
1728
2528
 
1729
-   message: "One or more attribute values are invalid",
2529
+ message: "One or more attribute values are invalid",
1730
2530
 
1731
-   errors: [
2531
+ errors: [
1732
2532
 
1733
-     {
2533
+ {
1734
2534
 
1735
-       path: "field",
2535
+ path: "field",
1736
2536
 
1737
-       code: "ERROR_CODE",
2537
+ code: "ERROR_CODE",
1738
2538
 
1739
-       message: "Validation error message"
2539
+ message: "Validation error message"
1740
2540
 
1741
-     }
2541
+ }
1742
2542
 
1743
-   ]
2543
+ ]
1744
2544
 
1745
2545
  }
1746
2546
  ```
@@ -1771,11 +2571,11 @@ An error can be returned as:
1771
2571
 
1772
2572
  {
1773
2573
 
1774
-   path: "email",
2574
+ path: "email",
1775
2575
 
1776
-   code: "INVALID_EMAIL",
2576
+ code: "INVALID_EMAIL",
1777
2577
 
1778
-   message: "Invalid email format for attribute email"
2578
+ message: "Invalid email format for attribute email"
1779
2579
 
1780
2580
  }
1781
2581
  ```
@@ -1834,13 +2634,13 @@ its complete nested path:
1834
2634
 
1835
2635
  {
1836
2636
 
1837
-   path: "address.location.latitude",
2637
+ path: "address.location.latitude",
1838
2638
 
1839
-   code: "INVALID_TYPE",
2639
+ code: "INVALID_TYPE",
1840
2640
 
1841
-   message:
2641
+ message:
1842
2642
 
1843
-     "Invalid type for attribute address.location.latitude, required number value"
2643
+ "Invalid type for attribute address.location.latitude, required number value"
1844
2644
 
1845
2645
  }
1846
2646
  ```
@@ -1888,13 +2688,13 @@ The invalid third element is reported as:
1888
2688
 
1889
2689
  {
1890
2690
 
1891
-   path: "marks[2]",
2691
+ path: "marks[2]",
1892
2692
 
1893
-   code: "OUT_OF_RANGE",
2693
+ code: "OUT_OF_RANGE",
1894
2694
 
1895
-   message:
2695
+ message:
1896
2696
 
1897
-     "Attribute marks[2] should have a value between 0 and 100"
2697
+ "Attribute marks[2] should have a value between 0 and 100"
1898
2698
 
1899
2699
  }
1900
2700
  ```
@@ -1910,7 +2710,9 @@ marks[1]
1910
2710
  marks[2]
1911
2711
  ```
1912
2712
 
1913
- Array-level constraints such as `minItems` and `maxItems` report the path of the array itself. For nested arrays, the complete parent path is retained, for example `orders[1].items`.
2713
+ Array-level constraints such as `minItems` and `maxItems` report the
2714
+ path of the array itself. For nested arrays, the complete parent path is
2715
+ retained, for example `orders[1].items`.
1914
2716
 
1915
2717
  ### Nested Fields Inside Arrays
1916
2718
 
@@ -1969,11 +2771,11 @@ Result:
1969
2771
 
1970
2772
  {
1971
2773
 
1972
-   "email": "Invalid email format for attribute email",
2774
+ "email": "Invalid email format for attribute email",
1973
2775
 
1974
-   "address.location.latitude": "Invalid type for attribute address.location.latitude, required number value",
2776
+ "address.location.latitude": "Invalid type for attribute address.location.latitude, required number value",
1975
2777
 
1976
-   "marks[2]": "Attribute marks[2] should have a value between 0 and 100"
2778
+ "marks[2]": "Attribute marks[2] should have a value between 0 and 100"
1977
2779
 
1978
2780
  }
1979
2781
  ```
@@ -1988,61 +2790,61 @@ sample-1
1988
2790
 
1989
2791
  {
1990
2792
 
1991
-   firstName: {
2793
+ firstName: {
1992
2794
 
1993
-     mandatory: true,
2795
+ mandatory: true,
1994
2796
 
1995
-     allowNull: false,
2797
+ allowNull: false,
1996
2798
 
1997
-     type: "string",
2799
+ type: "string",
1998
2800
 
1999
-     minLength: 3,
2801
+ minLength: 3,
2000
2802
 
2001
-     minLengthError: "First name must have minimum 3 characters."
2803
+ minLengthError: "First name must have minimum 3 characters."
2002
2804
 
2003
-   },
2805
+ },
2004
2806
 
2005
-   lastName: {
2807
+ lastName: {
2006
2808
 
2007
-     mandatory: false,
2809
+ mandatory: false,
2008
2810
 
2009
-     allowNull: true,
2811
+ allowNull: true,
2010
2812
 
2011
-     type: "string",
2813
+ type: "string",
2012
2814
 
2013
-   },
2815
+ },
2014
2816
 
2015
-   email: {
2817
+ email: {
2016
2818
 
2017
-     mandatory: true,
2819
+ mandatory: true,
2018
2820
 
2019
-     allowNull: false,
2821
+ allowNull: false,
2020
2822
 
2021
-     type: "email",
2823
+ type: "email",
2022
2824
 
2023
-   },
2825
+ },
2024
2826
 
2025
-   phone: {
2827
+ phone: {
2026
2828
 
2027
-     mandatory: true,
2829
+ mandatory: true,
2028
2830
 
2029
-     allowNull: false,
2831
+ allowNull: false,
2030
2832
 
2031
-     type: "string",
2833
+ type: "string",
2032
2834
 
2033
-   },
2835
+ },
2034
2836
 
2035
-   age: {
2837
+ age: {
2036
2838
 
2037
-     mandatory: false,
2839
+ mandatory: false,
2038
2840
 
2039
-     type: "number",
2841
+ type: "number",
2040
2842
 
2041
-     min: 1,
2843
+ min: 1,
2042
2844
 
2043
-     max: 120,
2845
+ max: 120,
2044
2846
 
2045
-   },
2847
+ },
2046
2848
 
2047
2849
  };
2048
2850
  ```
@@ -2053,271 +2855,271 @@ sample-2
2053
2855
 
2054
2856
  {
2055
2857
 
2056
-   id: {
2858
+ id: {
2057
2859
 
2058
-     mandatory: true,
2860
+ mandatory: true,
2059
2861
 
2060
-     allowNull: true,
2862
+ allowNull: true,
2061
2863
 
2062
-     type: "uuidv4",
2864
+ type: "uuidv4",
2063
2865
 
2064
-   },
2866
+ },
2065
2867
 
2066
-   batchId: {
2868
+ batchId: {
2067
2869
 
2068
-     mandatory: true,
2870
+ mandatory: true,
2069
2871
 
2070
-     allowNull: true,
2872
+ allowNull: true,
2071
2873
 
2072
-     type: "objectId",
2874
+ type: "objectId",
2073
2875
 
2074
-   },
2876
+ },
2075
2877
 
2076
-   firstName: {
2878
+ firstName: {
2077
2879
 
2078
-     mandatory: true,
2880
+ mandatory: true,
2079
2881
 
2080
-     type: "string",
2882
+ type: "string",
2081
2883
 
2082
-     minLength: 3,
2884
+ minLength: 3,
2083
2885
 
2084
-   },
2886
+ },
2085
2887
 
2086
-   lastName: {
2888
+ lastName: {
2087
2889
 
2088
-     mandatory: false,
2890
+ mandatory: false,
2089
2891
 
2090
-     allowNull: true,
2892
+ allowNull: true,
2091
2893
 
2092
-     type: "string",
2894
+ type: "string",
2093
2895
 
2094
-   },
2896
+ },
2095
2897
 
2096
-   age: {
2898
+ age: {
2097
2899
 
2098
-     type: "number",
2900
+ type: "number",
2099
2901
 
2100
-     min: 0.1,
2902
+ min: 0.1,
2101
2903
 
2102
-     max: 120,
2904
+ max: 120,
2103
2905
 
2104
-   },
2906
+ },
2105
2907
 
2106
-   isAdult: {
2908
+ isAdult: {
2107
2909
 
2108
-     type: "boolean",
2910
+ type: "boolean",
2109
2911
 
2110
-   },
2912
+ },
2111
2913
 
2112
-   totalWins: {
2914
+ totalWins: {
2113
2915
 
2114
-     type: "number",
2916
+ type: "number",
2115
2917
 
2116
-     min: 0,
2918
+ min: 0,
2117
2919
 
2118
-     preventDecimal: true,
2920
+ preventDecimal: true,
2119
2921
 
2120
-   },
2922
+ },
2121
2923
 
2122
-   email: {
2924
+ email: {
2123
2925
 
2124
-     regex: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$/,
2926
+ regex: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\\\\\.[a-zA-Z]{2,}$/,
2125
2927
 
2126
-   },
2928
+ },
2127
2929
 
2128
-   githubLink: {
2930
+ githubLink: {
2129
2931
 
2130
-     type: "url",
2932
+ type: "url",
2131
2933
 
2132
-   },
2934
+ },
2133
2935
 
2134
-   accountStatus: {
2936
+ accountStatus: {
2135
2937
 
2136
-     type: "enum",
2938
+ type: "enum",
2137
2939
 
2138
-     enumValues: ["Active", "Inactive", 200],
2940
+ enumValues: ["Active", "Inactive", 200],
2139
2941
 
2140
-   },
2942
+ },
2141
2943
 
2142
-   marks: {
2944
+ marks: {
2143
2945
 
2144
-     range: "0-100",
2946
+ range: "0-100",
2145
2947
 
2146
-   },
2948
+ },
2147
2949
 
2148
-   allMarks: {
2950
+ allMarks: {
2149
2951
 
2150
-     type: "array",
2952
+ type: "array",
2151
2953
 
2152
-     allowEmptyArray: false,
2954
+ allowEmptyArray: false,
2153
2955
 
2154
-     elementConstraints: {
2956
+ elementConstraints: {
2155
2957
 
2156
-       type: "number",
2958
+ type: "number",
2157
2959
 
2158
-       allowNull: false,
2960
+ allowNull: false,
2159
2961
 
2160
-       range: "0-100",
2962
+ range: "0-100",
2161
2963
 
2162
-     },
2964
+ },
2163
2965
 
2164
-   },
2966
+ },
2165
2967
 
2166
-   totalScore: {
2968
+ totalScore: {
2167
2969
 
2168
-     type: "number",
2970
+ type: "number",
2169
2971
 
2170
-     dependency: {
2972
+ dependency: {
2171
2973
 
2172
-       result: {
2974
+ result: {
2173
2975
 
2174
-         setDependencyRule: (totalScore, result) => {
2976
+ setDependencyRule: (totalScore, result) => {
2175
2977
 
2176
-           return { mandatory: true, allowNull: false, type: "string" };
2978
+ return { mandatory: true, allowNull: false, type: "string" };
2177
2979
 
2178
-         },
2980
+ },
2179
2981
 
2180
-       },
2982
+ },
2181
2983
 
2182
-     },
2984
+ },
2183
2985
 
2184
-   },
2986
+ },
2185
2987
 
2186
-   result: {
2988
+ result: {
2187
2989
 
2188
-     type: "string",
2990
+ type: "string",
2189
2991
 
2190
-     dependency: {
2992
+ dependency: {
2191
2993
 
2192
-       totalScore: {
2994
+ totalScore: {
2193
2995
 
2194
-         setDependencyRule: (result, totalScore) => {
2996
+ setDependencyRule: (result, totalScore) => {
2195
2997
 
2196
-           return { mandatory: true, allowNull: false, type: "number" };
2998
+ return { mandatory: true, allowNull: false, type: "number" };
2197
2999
 
2198
-         },
3000
+ },
2199
3001
 
2200
-       },
3002
+ },
2201
3003
 
2202
-     },
3004
+ },
2203
3005
 
2204
-   },
3006
+ },
2205
3007
 
2206
-   minSalary: {
3008
+ minSalary: {
2207
3009
 
2208
-     mandatory: true,
3010
+ mandatory: true,
2209
3011
 
2210
-     min: 1,
3012
+ min: 1,
2211
3013
 
2212
-     type: "number",
3014
+ type: "number",
2213
3015
 
2214
-     dependency: {
3016
+ dependency: {
2215
3017
 
2216
-       maxSalary: {
3018
+ maxSalary: {
2217
3019
 
2218
-         setDependencyRule: (minSalary, maxSalary) => {
3020
+ setDependencyRule: (minSalary, maxSalary) => {
2219
3021
 
2220
-           return {
3022
+ return {
2221
3023
 
2222
-             mandatory: true,
3024
+ mandatory: true,
2223
3025
 
2224
-             min: minSalary + 1,
3026
+ min: minSalary + 1,
2225
3027
 
2226
-             minError: "maxSalary must be more than minSalary",
3028
+ minError: "maxSalary must be more than minSalary",
2227
3029
 
2228
-           };
3030
+ };
2229
3031
 
2230
-         },
3032
+ },
2231
3033
 
2232
-       },
3034
+ },
2233
3035
 
2234
-     },
3036
+ },
2235
3037
 
2236
-   },
3038
+ },
2237
3039
 
2238
-   maxSalary: {
3040
+ maxSalary: {
2239
3041
 
2240
-     dependency: {
3042
+ dependency: {
2241
3043
 
2242
-       minSalary: {
3044
+ minSalary: {
2243
3045
 
2244
-         setDependencyRule: (maxSalary, minSalary) => {
3046
+ setDependencyRule: (maxSalary, minSalary) => {
2245
3047
 
2246
-           return {
3048
+ return {
2247
3049
 
2248
-             mandatory: true,
3050
+ mandatory: true,
2249
3051
 
2250
-             max: maxSalary - 1,
3052
+ max: maxSalary - 1,
2251
3053
 
2252
-             maxError: "minSalary must be less than maxSalary",
3054
+ maxError: "minSalary must be less than maxSalary",
2253
3055
 
2254
-           };
3056
+ };
2255
3057
 
2256
-         },
3058
+ },
2257
3059
 
2258
-       },
3060
+ },
2259
3061
 
2260
-     },
3062
+ },
2261
3063
 
2262
-   },
3064
+ },
2263
3065
 
2264
-   address: {
3066
+ address: {
2265
3067
 
2266
-     mandatory: true,
3068
+ mandatory: true,
2267
3069
 
2268
-     type: "object",
3070
+ type: "object",
2269
3071
 
2270
-     allowEmptyObject: false,
3072
+ allowEmptyObject: false,
2271
3073
 
2272
-     objectAttr: {
3074
+ objectAttr: {
2273
3075
 
2274
-       country: { mandatory: true, type: "string" },
3076
+ country: { mandatory: true, type: "string" },
2275
3077
 
2276
-       state: {
3078
+ state: {
2277
3079
 
2278
-         mandatory: true,
3080
+ mandatory: true,
2279
3081
 
2280
-         type: "string",
3082
+ type: "string",
2281
3083
 
2282
-       },
3084
+ },
2283
3085
 
2284
-       city: {},
3086
+ city: {},
2285
3087
 
2286
-       zip: {
3088
+ zip: {
2287
3089
 
2288
-         mandatory: true,
3090
+ mandatory: true,
2289
3091
 
2290
-         type: "string",
3092
+ type: "string",
2291
3093
 
2292
-       },
3094
+ },
2293
3095
 
2294
-       position: {
3096
+ position: {
2295
3097
 
2296
-         mandatory: true,
3098
+ mandatory: true,
2297
3099
 
2298
-         type: "object",
3100
+ type: "object",
2299
3101
 
2300
-         allowEmptyObject: false,
3102
+ allowEmptyObject: false,
2301
3103
 
2302
-         objectAttr: {
3104
+ objectAttr: {
2303
3105
 
2304
-           lattitude: { mandatory: true, type: "number" },
3106
+ lattitude: { mandatory: true, type: "number" },
2305
3107
 
2306
-           longitude: {
3108
+ longitude: {
2307
3109
 
2308
-             mandatory: true,
3110
+ mandatory: true,
2309
3111
 
2310
-             type: "number",
3112
+ type: "number",
2311
3113
 
2312
-           },
3114
+ },
2313
3115
 
2314
-         },
3116
+ },
2315
3117
 
2316
-       },
3118
+ },
2317
3119
 
2318
-     },
3120
+ },
2319
3121
 
2320
-   },
3122
+ },
2321
3123
 
2322
3124
  }
2323
3125
  ```
@@ -2328,15 +3130,15 @@ sample-2
2328
3130
 
2329
3131
  ```js
2330
3132
 
2331
- **// validatePayload is the middleware that invokes perfectPayload()**
3133
+ ***// validatePayload is the middleware that invokes perfectPayload()***
2332
3134
 
2333
3135
  router.post(
2334
3136
 
2335
-   "/payload-validation",
3137
+ "/payload-validation",
2336
3138
 
2337
-   validatePayload({ rule: <your validation rule json object> }),
3139
+ validatePayload({ rule: <your validation rule json object> }),
2338
3140
 
2339
-   (req, res) => res.send("OK")
3141
+ (req, res) => res.send("OK")
2340
3142
 
2341
3143
  );
2342
3144
  ```
@@ -2365,6 +3167,47 @@ export const validatePayload = ({ rule }) => {
2365
3167
  };
2366
3168
  ```
2367
3169
 
3170
+ #### Async ES Modules middleware example
3171
+
3172
+ When your schema contains an asynchronous `customValidator`, the
3173
+ middleware itself must be `async` and `perfectPayloadAsync()` must be
3174
+ awaited:
3175
+
3176
+ ```js
3177
+ import { perfectPayloadAsync } from "perfect-payload";
3178
+
3179
+ export const validatePayloadAsync = ({ rule }) => {
3180
+ return async (req, res, next) => {
3181
+ try {
3182
+ const { statusCode, ...response } = await perfectPayloadAsync(
3183
+ req?.body,
3184
+ rule,
3185
+ );
3186
+
3187
+ if (+statusCode >= 200 && +statusCode <= 299) {
3188
+ req.validatedBody = response?.validatedPayload;
3189
+ next();
3190
+ } else {
3191
+ res.status(statusCode).json(response);
3192
+ }
3193
+ } catch (error) {
3194
+ console.error("Error validating payload", error);
3195
+ res.status(500).json({ error: "Internal Server Error" });
3196
+ }
3197
+ };
3198
+ };
3199
+ ```
3200
+
3201
+ Route usage:
3202
+
3203
+ ```js
3204
+ router.post(
3205
+ "/payload-validation",
3206
+ validatePayloadAsync({ rule: <your validation rule json object> }),
3207
+ (req, res) => res.send("OK"),
3208
+ );
3209
+ ```
3210
+
2368
3211
  #### CommonJS middleware example
2369
3212
 
2370
3213
  ```js