perfect-payload 1.5.0-beta.0 → 1.6.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.
package/README.md CHANGED
@@ -1,22 +1,45 @@
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
+ - Extra payload fields are filtered from `validatedPayload`
28
+ - Legacy `perfectPayloadV1()` retained during the migration period
10
29
 
11
30
  ## Quick Links
12
31
 
13
32
  - [Installation](#installation)
14
33
  - [Basic Usage](#basic-usage)
34
+ - [Synchronous vs Asynchronous
35
+ Validation](#synchronous-vs-asynchronous-validation)
15
36
  - [Validation Rules](#validation-rules)
16
- - [Array Size and Nested Validation](#array-size-and-nested-validation)
37
+ - [Array Size and Nested
38
+ Validation](#array-size-and-nested-validation)
17
39
  - [Transformations and
18
40
  Sanitization](#transformations-and-sanitization)
19
41
  - [Custom Validators](#customvalidator)
42
+ - [Asynchronous Validation](#asynchronous-validation)
20
43
  - [Error Codes](#error-codes)
21
44
  - [Custom Error Messages](#custom-error-messages)
22
45
  - [Nested Objects and Array Field
@@ -79,24 +102,25 @@ console.log(result);
79
102
 
80
103
  {
81
104
 
82
-   statusCode: 200,
105
+ statusCode: 200,
83
106
 
84
-   valid: true,
107
+ valid: true,
85
108
 
86
-   validatedPayload: {
109
+ validatedPayload: {
87
110
 
88
-     name: "Kiran",
111
+ name: "Kiran",
89
112
 
90
-     email: "kiran@example.com",
113
+ email: "kiran@example.com",
91
114
 
92
-     age: 29
115
+ age: 29
93
116
 
94
-   }
117
+ }
95
118
 
96
119
  }
97
120
  ```
98
121
 
99
122
  Note: The validatedPayload contains only the fields
123
+
100
124
  defined in the
101
125
 
102
126
  schema, automatically filtering out any extra attributes. You can use it
@@ -111,25 +135,25 @@ to safely overwrite request.body or assign it to a new request property
111
135
 
112
136
  {
113
137
 
114
-   statusCode: 400,
138
+ statusCode: 400,
115
139
 
116
-   valid: false,
140
+ valid: false,
117
141
 
118
-   message: "One or more attribute values are invalid",
142
+ message: "One or more attribute values are invalid",
119
143
 
120
-   errors: [
144
+ errors: [
121
145
 
122
-     {
146
+ {
123
147
 
124
-       path: "email",
148
+ path: "email",
125
149
 
126
-       code: "INVALID_EMAIL",
150
+ code: "INVALID_EMAIL",
127
151
 
128
-       message: "Invalid email format for attribute email"
152
+ message: "Invalid email format for attribute email"
129
153
 
130
-     }
154
+ }
131
155
 
132
-   ]
156
+ ]
133
157
 
134
158
  }
135
159
  ```
@@ -140,11 +164,11 @@ Each error returned by `perfectPayload()` contains:
140
164
 
141
165
  {
142
166
 
143
-   path: "field.path",
167
+ path: "field.path",
144
168
 
145
-   code: "ERROR_CODE",
169
+ code: "ERROR_CODE",
146
170
 
147
-   message: "Human readable validation message"
171
+ message: "Human readable validation message"
148
172
 
149
173
  }
150
174
  ```
@@ -159,6 +183,55 @@ failure.
159
183
 
160
184
  \- Submitted payload values are not included in default error messages.
161
185
 
186
+ ## Synchronous vs Asynchronous Validation
187
+
188
+ For normal synchronous validation, use `perfectPayload()`:
189
+
190
+ ```js
191
+ import { perfectPayload } from "perfect-payload";
192
+
193
+ const result = perfectPayload(payload, validationRules);
194
+ ```
195
+
196
+ When any `customValidator` needs to perform asynchronous work, use
197
+ `perfectPayloadAsync()` and `await` the result:
198
+
199
+ ```js
200
+ import { perfectPayloadAsync } from "perfect-payload";
201
+
202
+ const result = await perfectPayloadAsync(payload, validationRules);
203
+ ```
204
+
205
+ The public APIs are:
206
+
207
+ ```text
208
+ perfectPayloadV1() legacy API; deprecated
209
+ perfectPayload() synchronous validation
210
+ perfectPayloadAsync() synchronous + asynchronous customValidator
211
+ ```
212
+
213
+ `perfectPayload()` remains synchronous and intentionally rejects a
214
+ `customValidator` that returns a Promise. This preserves the existing
215
+ synchronous API contract.
216
+
217
+ `perfectPayloadAsync()` first performs transformations and normal
218
+ synchronous validation. If synchronous validation fails, the result is
219
+ returned immediately and asynchronous validators are not executed. This
220
+ avoids unnecessary asynchronous work for payloads that are already
221
+ invalid.
222
+
223
+ ```text
224
+ transformations
225
+
226
+ synchronous validation
227
+
228
+ sync errors? ── yes ──→ return validation errors
229
+ ↓ no
230
+ async customValidator
231
+
232
+ return result
233
+ ```
234
+
162
235
  ## Legacy API
163
236
 
164
237
  `perfectPayloadV1()` is still available for backward compatibility.
@@ -200,6 +273,7 @@ errors: [
200
273
  ```
201
274
 
202
275
  Note: If an inValidPayloadResponse is provided, the
276
+
203
277
  system returns
204
278
 
205
279
  it alongside an automatically generated errors property. Do not include
@@ -211,6 +285,7 @@ object.
211
285
  ## Validation Rules
212
286
 
213
287
  `perfectPayload()` supports validation, nested-schema,
288
+
214
289
  custom-validation, and transformation rules.
215
290
 
216
291
  ### `mandatory`
@@ -305,6 +380,7 @@ Default: Not applied when omitted.
305
380
  const rules = {
306
381
  tags: {
307
382
  type: "array",
383
+
308
384
  minItems: 2,
309
385
  },
310
386
  };
@@ -313,14 +389,20 @@ const rules = {
313
389
  An array with fewer than 2 items returns `MIN_ITEMS`.
314
390
 
315
391
  ```js
392
+
316
393
  {
394
+
317
395
  path: "tags",
396
+
318
397
  code: "MIN_ITEMS",
398
+
319
399
  message: "Attribute tags must contain at least 2 item(s)"
400
+
320
401
  }
321
402
  ```
322
403
 
323
- `minItems` is enforced even when `allowEmptyArray: true` is set. For example, `minItems: 2` still rejects `[]`.
404
+ `minItems` is enforced even when `allowEmptyArray: true` is set. For
405
+ example, `minItems: 2` still rejects `[]`.
324
406
 
325
407
  Error code: `MIN_ITEMS`
326
408
 
@@ -336,6 +418,7 @@ Default: Not applied when omitted.
336
418
  const rules = {
337
419
  tags: {
338
420
  type: "array",
421
+
339
422
  maxItems: 5,
340
423
  },
341
424
  };
@@ -344,10 +427,15 @@ const rules = {
344
427
  An array with more than 5 items returns `MAX_ITEMS`.
345
428
 
346
429
  ```js
430
+
347
431
  {
432
+
348
433
  path: "tags",
434
+
349
435
  code: "MAX_ITEMS",
436
+
350
437
  message: "Attribute tags must contain at most 5 item(s)"
438
+
351
439
  }
352
440
  ```
353
441
 
@@ -554,6 +642,7 @@ Error code: `MAX_LENGTH`
554
642
  Prevents decimal numbers.
555
643
 
556
644
  Default: `false`; both integer and decimal numbers are
645
+
557
646
  allowed.
558
647
 
559
648
  Example:
@@ -664,13 +753,13 @@ Example error:
664
753
 
665
754
  {
666
755
 
667
-   path: "marks[2]",
756
+ path: "marks[2]",
668
757
 
669
-   code: "OUT_OF_RANGE",
758
+ code: "OUT_OF_RANGE",
670
759
 
671
-   message:
760
+ message:
672
761
 
673
-     "Attribute marks[2] should have a value between 0 and 100"
762
+ "Attribute marks[2] should have a value between 0 and 100"
674
763
 
675
764
  }
676
765
  ```
@@ -739,13 +828,13 @@ Nested errors include the complete field path:
739
828
 
740
829
  {
741
830
 
742
-   path: "address.location.latitude",
831
+ path: "address.location.latitude",
743
832
 
744
-   code: "INVALID_TYPE",
833
+ code: "INVALID_TYPE",
745
834
 
746
-   message:
835
+ message:
747
836
 
748
-     "Invalid type for attribute address.location.latitude, required number value"
837
+ "Invalid type for attribute address.location.latitude, required number value"
749
838
 
750
839
  }
751
840
  ```
@@ -784,13 +873,13 @@ Example error:
784
873
 
785
874
  {
786
875
 
787
-   path: "maxSalary",
876
+ path: "maxSalary",
788
877
 
789
-   code: "MIN_VALUE",
878
+ code: "MIN_VALUE",
790
879
 
791
-   message:
880
+ message:
792
881
 
793
-     "maxSalary must be more than minSalary"
882
+ "maxSalary must be more than minSalary"
794
883
 
795
884
  }
796
885
  ```
@@ -799,7 +888,9 @@ Example error:
799
888
 
800
889
  ## Array Size and Nested Validation
801
890
 
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.
891
+ `perfectPayload()` supports array size constraints and recursive
892
+ validation of arrays and objects at multiple depths. Array indexes and
893
+ nested object keys are preserved in structured error paths.
803
894
 
804
895
  ### Array size constraints
805
896
 
@@ -809,12 +900,17 @@ Use `minItems` and `maxItems` with `type: "array"`:
809
900
  const rules = {
810
901
  products: {
811
902
  type: "array",
903
+
812
904
  minItems: 1,
905
+
813
906
  maxItems: 3,
907
+
814
908
  elementConstraints: {
815
909
  type: "object",
910
+
816
911
  objectAttr: {
817
912
  productId: { mandatory: true, type: "string" },
913
+
818
914
  quantity: { mandatory: true, type: "number", min: 1 },
819
915
  },
820
916
  },
@@ -825,18 +921,26 @@ const rules = {
825
921
  If the array is empty, `minItems` reports the array path itself:
826
922
 
827
923
  ```js
924
+
828
925
  {
926
+
829
927
  path: "products",
928
+
830
929
  code: "MIN_ITEMS",
930
+
831
931
  message: "Attribute products must contain at least 1 item(s)"
932
+
832
933
  }
833
934
  ```
834
935
 
835
936
  ### Arrays of objects
836
937
 
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:
938
+ `elementConstraints` can contain `objectAttr`, allowing every object in
939
+ an array to use a nested schema. An invalid quantity in the second
940
+ product is reported as:
838
941
 
839
942
  ```text
943
+
840
944
  products[1].quantity
841
945
  ```
842
946
 
@@ -848,21 +952,32 @@ products[1].quantity
848
952
  const rules = {
849
953
  orders: {
850
954
  type: "array",
955
+
851
956
  minItems: 1,
957
+
852
958
  maxItems: 2,
959
+
853
960
  elementConstraints: {
854
961
  type: "object",
962
+
855
963
  objectAttr: {
856
964
  orderId: { mandatory: true, type: "string" },
965
+
857
966
  items: {
858
967
  mandatory: true,
968
+
859
969
  type: "array",
970
+
860
971
  minItems: 1,
972
+
861
973
  maxItems: 2,
974
+
862
975
  elementConstraints: {
863
976
  type: "object",
977
+
864
978
  objectAttr: {
865
979
  productId: { mandatory: true, type: "string" },
980
+
866
981
  quantity: { mandatory: true, type: "number", min: 1 },
867
982
  },
868
983
  },
@@ -873,31 +988,41 @@ const rules = {
873
988
  };
874
989
  ```
875
990
 
876
- A deep validation failure preserves the complete indexed path, for example:
991
+ A deep validation failure preserves the complete indexed path, for
992
+ example:
877
993
 
878
994
  ```text
995
+
879
996
  orders[1].items[2].quantity
880
997
  ```
881
998
 
882
- Array constraints work at nested levels too. A nested array can report paths such as:
999
+ Array constraints work at nested levels too. A nested array can report
1000
+ paths such as:
883
1001
 
884
1002
  ```text
1003
+
885
1004
  orders[1].items
886
1005
  ```
887
1006
 
888
1007
  Nested arrays are supported and every array index is preserved:
889
1008
 
890
1009
  ```text
1010
+
891
1011
  matrix[1][1]
1012
+
892
1013
  matrix[1][1][1]
893
1014
  ```
894
1015
 
895
- Transformations applied inside nested objects or array elements are preserved in `validatedPayload`, while the original input remains unchanged.
1016
+ Transformations applied inside nested objects or array elements are
1017
+ preserved in `validatedPayload`, while the original input remains
1018
+ unchanged.
896
1019
 
897
1020
  ### Transformations and Sanitization
898
1021
 
899
1022
  `perfectPayload()` can transform a field before its validation rules
1023
+
900
1024
  run. The transformed value is returned in `validatedPayload`, while the
1025
+
901
1026
  original input object is not mutated.
902
1027
 
903
1028
  Supported transformation rules:
@@ -907,53 +1032,81 @@ Rule Purpose
907
1032
  ---
908
1033
 
909
1034
  `trim` Removes leading and trailing whitespace from strings
1035
+
910
1036
  `lowercase` Converts strings to lowercase
1037
+
911
1038
  `uppercase` Converts strings to uppercase
1039
+
912
1040
  `transform` Runs a custom synchronous transformation function
913
1041
 
914
1042
  Transformations always run in this fixed order, regardless of the order
1043
+
915
1044
  in which the rule properties are written:
916
1045
 
917
1046
  ```text
1047
+
918
1048
  trim
1049
+
919
1050
 
1051
+
920
1052
  lowercase
1053
+
921
1054
 
1055
+
922
1056
  uppercase
1057
+
923
1058
 
1059
+
924
1060
  transform(value, payload)
1061
+
925
1062
 
1063
+
926
1064
  validation rules
1065
+
927
1066
 
1067
+
928
1068
  customValidator
1069
+
929
1070
 
1071
+
930
1072
  validatedPayload
931
1073
  ```
932
1074
 
933
1075
  #### `trim`
934
1076
 
935
1077
  ```js
1078
+
936
1079
  const payload = {
1080
+
937
1081
  name: " Kiran Poojary ",
1082
+
938
1083
  };
939
1084
 
940
1085
  const rules = {
1086
+
941
1087
  name: {
1088
+
942
1089
  type: "string",
1090
+
943
1091
  trim: true,
1092
+
944
1093
  },
1094
+
945
1095
  };
946
1096
 
947
1097
  const result = perfectPayload(payload, rules);
948
1098
 
949
1099
  console.log(result.validatedPayload.name);
950
- // "Kiran Poojary"
1100
+
1101
+ *// "Kiran Poojary"*
951
1102
 
952
1103
  console.log(payload.name);
953
- // " Kiran Poojary "
1104
+
1105
+ *// " Kiran Poojary "*
954
1106
  ```
955
1107
 
956
1108
  `trim` applies only to string values. Non-string values are left
1109
+
957
1110
  unchanged.
958
1111
 
959
1112
  #### `lowercase`
@@ -962,13 +1115,16 @@ unchanged.
962
1115
  const rules = {
963
1116
  email: {
964
1117
  trim: true,
1118
+
965
1119
  lowercase: true,
1120
+
966
1121
  type: "email",
967
1122
  },
968
1123
  };
969
1124
  ```
970
1125
 
971
1126
  For `" KIRAN@EXAMPLE.COM "`, the validated value becomes
1127
+
972
1128
  `"kiran@example.com"`.
973
1129
 
974
1130
  #### `uppercase`
@@ -977,6 +1133,7 @@ For `" KIRAN@EXAMPLE.COM "`, the validated value becomes
977
1133
  const rules = {
978
1134
  countryCode: {
979
1135
  type: "string",
1136
+
980
1137
  uppercase: true,
981
1138
  },
982
1139
  };
@@ -985,6 +1142,7 @@ const rules = {
985
1142
  For `"in"`, the validated value becomes `"IN"`.
986
1143
 
987
1144
  `lowercase: true` and `uppercase: true` cannot be enabled together for
1145
+
988
1146
  the same field. Doing so throws a schema configuration error.
989
1147
 
990
1148
  #### `transform`
@@ -995,6 +1153,7 @@ Use `transform` when the built-in string transformations are not enough.
995
1153
  const rules = {
996
1154
  phone: {
997
1155
  type: "string",
1156
+
998
1157
  transform: (value) => value.replace(/\s+/g, ""),
999
1158
  },
1000
1159
  };
@@ -1011,31 +1170,46 @@ transform: (value, payload) => {
1011
1170
  ```
1012
1171
 
1013
1172
  - `value` is the field value after the built-in transformations have
1173
+
1014
1174
  run.
1175
+
1015
1176
  - `payload` is the current payload/object being validated.
1016
1177
 
1017
1178
  This makes cross-field transformations possible:
1018
1179
 
1019
1180
  ```js
1181
+
1020
1182
  const payload = {
1183
+
1021
1184
  amount: 100,
1185
+
1022
1186
  multiplier: 2,
1187
+
1023
1188
  };
1024
1189
 
1025
1190
  const rules = {
1191
+
1026
1192
  amount: {
1193
+
1027
1194
  transform: (value, payload) => value * payload.multiplier,
1195
+
1028
1196
  type: "number",
1197
+
1029
1198
  },
1199
+
1030
1200
  multiplier: {
1201
+
1031
1202
  type: "number",
1203
+
1032
1204
  },
1205
+
1033
1206
  };
1034
1207
 
1035
1208
  const result = perfectPayload(payload, rules);
1036
1209
 
1037
1210
  console.log(result.validatedPayload.amount);
1038
- // 200
1211
+
1212
+ *// 200*
1039
1213
  ```
1040
1214
 
1041
1215
  A custom transformer may also change the data type before validation:
@@ -1044,36 +1218,48 @@ A custom transformer may also change the data type before validation:
1044
1218
  const rules = {
1045
1219
  quantity: {
1046
1220
  transform: (value) => Number(value),
1221
+
1047
1222
  type: "number",
1223
+
1048
1224
  min: 1,
1225
+
1049
1226
  max: 100,
1050
1227
  },
1051
1228
  };
1052
1229
  ```
1053
1230
 
1054
1231
  The transformed value is validated by the normal validation rules and is
1232
+
1055
1233
  also the value received by `customValidator`.
1056
1234
 
1057
1235
  Transformations work inside `objectAttr` and `elementConstraints`, and
1236
+
1058
1237
  transformed nested/array values are preserved in `validatedPayload`.
1059
1238
 
1060
1239
  ```js
1061
1240
  const rules = {
1062
1241
  profile: {
1063
1242
  type: "object",
1243
+
1064
1244
  objectAttr: {
1065
1245
  name: {
1066
1246
  trim: true,
1247
+
1067
1248
  uppercase: true,
1249
+
1068
1250
  type: "string",
1069
1251
  },
1070
1252
  },
1071
1253
  },
1254
+
1072
1255
  tags: {
1073
1256
  type: "array",
1257
+
1074
1258
  elementConstraints: {
1075
1259
  trim: true,
1260
+
1076
1261
  lowercase: true,
1262
+
1077
1263
  type: "string",
1078
1264
  },
1079
1265
  },
@@ -1081,29 +1267,39 @@ const rules = {
1081
1267
  ```
1082
1268
 
1083
1269
  Missing optional fields are not transformed. An input value of `null` is
1270
+
1084
1271
  not passed to transformation functions; null handling remains controlled
1272
+
1085
1273
  by `allowNull`.
1086
1274
 
1087
- **Important:** `transform` is synchronous. A non-function transformer,
1275
+ \*\*\*\*Important:\*\*\*\* `transform` is synchronous. A non-function
1276
+ transformer,
1277
+
1088
1278
  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
1279
+
1280
+ transformer that returns `undefined` is not supported and throws an
1281
+ error.
1282
+
1283
+ Returning `null`, `""`, `0`, or `false` is allowed; the transformed
1284
+ value is
1285
+
1286
+ then processed by the normal validation rules. Exceptions thrown inside
1287
+ the
1288
+
1092
1289
  transformer propagate to the caller.
1093
1290
 
1094
1291
  For example, returning `undefined` throws:
1095
1292
 
1096
1293
  ```text
1294
+
1097
1295
  perfect-payload:- transform must not return undefined for attribute username
1098
1296
  ```
1099
1297
 
1100
1298
  ### `customValidator`
1101
1299
 
1102
- Allows you to define custom synchronous validation logic for a field
1103
- when the built-in validation rules are not enough.
1300
+ Defines custom validation logic when the built-in rules are not enough.
1104
1301
 
1105
- The validator receives the field value and the current payload/object
1106
- being validated:
1302
+ The validator receives:
1107
1303
 
1108
1304
  ```js
1109
1305
  customValidator: (value, payload) => {
@@ -1111,100 +1307,302 @@ customValidator: (value, payload) => {
1111
1307
  };
1112
1308
  ```
1113
1309
 
1114
- The validator must return `true` to pass validation. Any other return
1115
- value causes validation to fail.
1310
+ - `value` is the field value after transformations have been applied.
1311
+ - `payload` is the current payload/object being validated.
1312
+ - Return `true` to pass.
1313
+ - Any value other than `true` fails validation.
1314
+ - Exceptions thrown by the validator propagate to the caller.
1116
1315
 
1117
- Example:
1316
+ For nested validation, `payload` means the current nested object rather
1317
+ than the root request body.
1318
+
1319
+ #### Synchronous custom validator
1320
+
1321
+ Use a synchronous validator with `perfectPayload()`:
1118
1322
 
1119
1323
  ```js
1120
1324
  const rules = {
1121
1325
  username: {
1122
1326
  mandatory: true,
1123
-
1124
1327
  type: "string",
1328
+ trim: true,
1125
1329
 
1126
1330
  customValidator: (value) => {
1127
1331
  return !value.toLowerCase().includes("admin");
1128
1332
  },
1129
1333
 
1130
1334
  customValidatorCode: "RESERVED_USERNAME",
1131
-
1132
1335
  customValidatorError: "Username cannot contain admin",
1133
1336
  },
1134
1337
  };
1338
+
1339
+ const result = perfectPayload({ username: " admin_kiran " }, rules);
1135
1340
  ```
1136
1341
 
1137
- For this payload:
1342
+ A failure returns:
1138
1343
 
1139
1344
  ```js
1140
- const payload = {
1141
- username: "admin_kiran",
1142
- };
1345
+ {
1346
+ statusCode: 400,
1347
+ valid: false,
1348
+ message: "One or more attribute values are invalid",
1349
+ errors: [
1350
+ {
1351
+ path: "username",
1352
+ code: "RESERVED_USERNAME",
1353
+ message: "Username cannot contain admin"
1354
+ }
1355
+ ]
1356
+ }
1143
1357
  ```
1144
1358
 
1145
- The validation error is:
1359
+ The current payload/object can be used for cross-field validation:
1146
1360
 
1147
1361
  ```js
1362
+ const rules = {
1363
+ limit: {
1364
+ type: "number",
1365
+ },
1148
1366
 
1149
- {
1367
+ amount: {
1368
+ type: "number",
1150
1369
 
1151
-   path: "username",
1370
+ customValidator: (value, payload) => {
1371
+ return value <= payload.limit;
1372
+ },
1152
1373
 
1153
-   code: "RESERVED_USERNAME",
1374
+ customValidatorCode: "LIMIT_EXCEEDED",
1375
+ customValidatorError: "Amount cannot exceed limit",
1376
+ },
1377
+ };
1378
+ ```
1154
1379
 
1155
-   message: "Username cannot contain admin"
1380
+ If `customValidatorCode` and `customValidatorError` are omitted, the
1381
+ default error is:
1156
1382
 
1383
+ ```js
1384
+ {
1385
+ path: "username",
1386
+ code: "CUSTOM_VALIDATION_FAILED",
1387
+ message: "Custom validation failed for attribute username"
1157
1388
  }
1158
1389
  ```
1159
1390
 
1160
- If `customValidatorCode` and `customValidatorError` are not provided,
1161
- the default error is:
1391
+ `customValidator` works recursively inside `objectAttr` and
1392
+ `elementConstraints`. Structured errors preserve the corresponding
1393
+ nested and array paths.
1394
+
1395
+ When using `perfectPayload()`, `customValidator` must remain
1396
+ synchronous. A Promise-returning validator throws:
1397
+
1398
+ ```text
1399
+ perfect-payload:- customValidator must be synchronous for attribute username
1400
+ ```
1401
+
1402
+ For asynchronous custom validation, use `perfectPayloadAsync()`.
1403
+
1404
+ ## Asynchronous Validation
1405
+
1406
+ `perfectPayloadAsync()` supports both synchronous and asynchronous
1407
+ `customValidator` functions without changing the behavior of
1408
+ `perfectPayload()`.
1162
1409
 
1163
1410
  ```js
1411
+ import { perfectPayloadAsync } from "perfect-payload";
1164
1412
 
1165
- {
1413
+ const rules = {
1414
+ username: {
1415
+ mandatory: true,
1416
+ type: "string",
1417
+ trim: true,
1166
1418
 
1167
-   path: "username",
1419
+ customValidator: async (value) => {
1420
+ const available = await checkUsernameAvailability(value);
1421
+ return available;
1422
+ },
1168
1423
 
1169
-   code: "CUSTOM_VALIDATION_FAILED",
1424
+ customValidatorCode: "USERNAME_TAKEN",
1425
+ customValidatorError: "Username is already taken",
1426
+ },
1427
+ };
1170
1428
 
1171
-   message: "Custom validation failed for attribute username"
1429
+ const result = await perfectPayloadAsync(
1430
+ {
1431
+ username: " kiran ",
1432
+ },
1433
+ rules,
1434
+ );
1435
+ ```
1172
1436
 
1437
+ On success, transformations are preserved:
1438
+
1439
+ ```js
1440
+ {
1441
+ statusCode: 200,
1442
+ valid: true,
1443
+ validatedPayload: {
1444
+ username: "kiran"
1445
+ }
1446
+ }
1447
+ ```
1448
+
1449
+ On asynchronous validation failure:
1450
+
1451
+ ```js
1452
+ {
1453
+ statusCode: 400,
1454
+ valid: false,
1455
+ message: "One or more attribute values are invalid",
1456
+ errors: [
1457
+ {
1458
+ path: "username",
1459
+ code: "USERNAME_TAKEN",
1460
+ message: "Username is already taken"
1461
+ }
1462
+ ]
1173
1463
  }
1174
1464
  ```
1175
1465
 
1176
- The current payload/object being validated can be used as the second
1177
- argument when required:
1466
+ ### Async validator contract
1467
+
1468
+ For `perfectPayloadAsync()`:
1469
+
1470
+ ```text
1471
+ true → pass
1472
+ false → validation failure
1473
+ anything != true → validation failure
1474
+ throw → exception propagates
1475
+ rejected Promise → rejection propagates
1476
+ ```
1477
+
1478
+ A normal synchronous validator is also valid when using the asynchronous
1479
+ API:
1178
1480
 
1179
1481
  ```js
1180
1482
  const rules = {
1181
- limit: {
1182
- type: "number",
1483
+ username: {
1484
+ type: "string",
1485
+ customValidator: (value) => value !== "admin",
1183
1486
  },
1487
+ };
1184
1488
 
1185
- amount: {
1186
- type: "number",
1489
+ const result = await perfectPayloadAsync(payload, rules);
1490
+ ```
1187
1491
 
1188
- customValidator: (value, payload) => {
1189
- return value <= payload.limit;
1492
+ A configured `customValidator` must be a function. Otherwise an error is
1493
+ thrown:
1494
+
1495
+ ```text
1496
+ perfect-payload:- customValidator must be a function for attribute username
1497
+ ```
1498
+
1499
+ ### Validation order
1500
+
1501
+ `perfectPayloadAsync()` uses two phases:
1502
+
1503
+ 1. Transform the payload and run normal synchronous validation.
1504
+ 2. If phase 1 succeeds, run custom validators with `await`.
1505
+
1506
+ If any synchronous validation error exists, phase 2 is skipped and the
1507
+ synchronous validation result is returned immediately.
1508
+
1509
+ This means asynchronous validators can assume the payload has already
1510
+ passed its normal synchronous validation rules.
1511
+
1512
+ ### Nested async validation
1513
+
1514
+ Async custom validators work recursively inside `objectAttr`:
1515
+
1516
+ ```js
1517
+ const rules = {
1518
+ profile: {
1519
+ type: "object",
1520
+
1521
+ objectAttr: {
1522
+ username: {
1523
+ type: "string",
1524
+ trim: true,
1525
+
1526
+ customValidator: async (value) => {
1527
+ return await isUsernameAvailable(value);
1528
+ },
1529
+
1530
+ customValidatorCode: "USERNAME_TAKEN",
1531
+ customValidatorError: "Username is already taken",
1532
+ },
1190
1533
  },
1534
+ },
1535
+ };
1536
+ ```
1191
1537
 
1192
- customValidatorCode: "LIMIT_EXCEEDED",
1538
+ A failure produces the complete path:
1193
1539
 
1194
- customValidatorError: "Amount cannot exceed limit",
1540
+ ```text
1541
+ profile.username
1542
+ ```
1543
+
1544
+ They also work inside `elementConstraints`:
1545
+
1546
+ ```js
1547
+ const rules = {
1548
+ usernames: {
1549
+ type: "array",
1550
+
1551
+ elementConstraints: {
1552
+ type: "string",
1553
+ trim: true,
1554
+
1555
+ customValidator: async (value) => {
1556
+ return await isUsernameAvailable(value);
1557
+ },
1558
+
1559
+ customValidatorCode: "USERNAME_TAKEN",
1560
+ customValidatorError: "Username is already taken",
1561
+ },
1195
1562
  },
1196
1563
  };
1197
1564
  ```
1198
1565
 
1199
- `customValidator` also works with nested objects and array
1200
- `elementConstraints`. The generated structured error automatically
1201
- contains the corresponding nested or array path.
1566
+ For an invalid second element:
1567
+
1568
+ ```text
1569
+ usernames[1]
1570
+ ```
1571
+
1572
+ Deep combinations of objects and arrays preserve every level of the
1573
+ path:
1574
+
1575
+ ```text
1576
+ products[1].seller.username
1577
+ profile.teams[1].members[1].username
1578
+ ```
1579
+
1580
+ Default async custom-validation messages also use the final indexed
1581
+ path:
1582
+
1583
+ ```js
1584
+ {
1585
+ path: "users[1].username",
1586
+ code: "CUSTOM_VALIDATION_FAILED",
1587
+ message: "Custom validation failed for attribute users[1].username"
1588
+ }
1589
+ ```
1590
+
1591
+ ### Transform remains synchronous
1202
1592
 
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.
1593
+ `perfectPayloadAsync()` makes custom validation asynchronous; it does
1594
+ not make `transform` asynchronous.
1206
1595
 
1207
- Error code when no custom code is provided: `CUSTOM_VALIDATION_FAILED`
1596
+ `transform` must still be synchronous:
1597
+
1598
+ ```js
1599
+ transform: (value, payload) => {
1600
+ return value;
1601
+ };
1602
+ ```
1603
+
1604
+ An async transformer or a transformer that returns a Promise is not
1605
+ supported.
1208
1606
 
1209
1607
  ## Error Codes
1210
1608
 
@@ -1277,17 +1675,17 @@ const result = perfectPayload(payload, validationRules);
1277
1675
 
1278
1676
  if (!result.valid) {
1279
1677
 
1280
-   const emailError = result.errors.find(
1678
+ const emailError = result.errors.find(
1281
1679
 
1282
-     (error) => error.code === "INVALID_EMAIL",
1680
+ (error) => error.code === "INVALID_EMAIL",
1283
1681
 
1284
-   );
1682
+ );
1285
1683
 
1286
-   if (emailError) {
1684
+ if (emailError) {
1287
1685
 
1288
-     **// Handle invalid email**
1686
+ ***// Handle invalid email***
1289
1687
 
1290
-   }
1688
+ }
1291
1689
 
1292
1690
  }
1293
1691
  ```
@@ -1304,11 +1702,11 @@ keeping the same structured error format:
1304
1702
 
1305
1703
  {
1306
1704
 
1307
-   path: "email",
1705
+ path: "email",
1308
1706
 
1309
-   code: "INVALID_EMAIL",
1707
+ code: "INVALID_EMAIL",
1310
1708
 
1311
-   message: "Email address is invalid"
1709
+ message: "Email address is invalid"
1312
1710
 
1313
1711
  }
1314
1712
  ```
@@ -1335,11 +1733,11 @@ If `email` is missing:
1335
1733
 
1336
1734
  {
1337
1735
 
1338
-   path: "email",
1736
+ path: "email",
1339
1737
 
1340
-   code: "REQUIRED",
1738
+ code: "REQUIRED",
1341
1739
 
1342
-   message: "Email is required"
1740
+ message: "Email is required"
1343
1741
 
1344
1742
  }
1345
1743
  ```
@@ -1350,11 +1748,11 @@ If `email` is present but invalid:
1350
1748
 
1351
1749
  {
1352
1750
 
1353
-   path: "email",
1751
+ path: "email",
1354
1752
 
1355
-   code: "INVALID_EMAIL",
1753
+ code: "INVALID_EMAIL",
1356
1754
 
1357
-   message: "Email address is invalid"
1755
+ message: "Email address is invalid"
1358
1756
 
1359
1757
  }
1360
1758
  ```
@@ -1443,53 +1841,53 @@ Example result:
1443
1841
 
1444
1842
  {
1445
1843
 
1446
-   statusCode: 400,
1844
+ statusCode: 400,
1447
1845
 
1448
-   valid: false,
1846
+ valid: false,
1449
1847
 
1450
-   message:
1848
+ message:
1451
1849
 
1452
-     "One or more attribute values are invalid",
1850
+ "One or more attribute values are invalid",
1453
1851
 
1454
-   errors: [
1852
+ errors: [
1455
1853
 
1456
-     {
1854
+ {
1457
1855
 
1458
-       path: "username",
1856
+ path: "username",
1459
1857
 
1460
-       code: "MIN_LENGTH",
1858
+ code: "MIN_LENGTH",
1461
1859
 
1462
-       message:
1860
+ message:
1463
1861
 
1464
-         "Username must contain at least 3 characters"
1862
+ "Username must contain at least 3 characters"
1465
1863
 
1466
-     },
1864
+ },
1467
1865
 
1468
-     {
1866
+ {
1469
1867
 
1470
-       path: "age",
1868
+ path: "age",
1471
1869
 
1472
-       code: "MIN_VALUE",
1870
+ code: "MIN_VALUE",
1473
1871
 
1474
-       message:
1872
+ message:
1475
1873
 
1476
-         "Age must be at least 18"
1874
+ "Age must be at least 18"
1477
1875
 
1478
-     },
1876
+ },
1479
1877
 
1480
-     {
1878
+ {
1481
1879
 
1482
-       path: "score",
1880
+ path: "score",
1483
1881
 
1484
-       code: "OUT_OF_RANGE",
1882
+ code: "OUT_OF_RANGE",
1485
1883
 
1486
-       message:
1884
+ message:
1487
1885
 
1488
-         "Score must be between 0 and 100"
1886
+ "Score must be between 0 and 100"
1489
1887
 
1490
-     }
1888
+ }
1491
1889
 
1492
-   ]
1890
+ ]
1493
1891
 
1494
1892
  }
1495
1893
  ```
@@ -1520,11 +1918,11 @@ Still returns:
1520
1918
 
1521
1919
  {
1522
1920
 
1523
-   path: "age",
1921
+ path: "age",
1524
1922
 
1525
-   code: "MIN_VALUE",
1923
+ code: "MIN_VALUE",
1526
1924
 
1527
-   message: "You must be 18 or older"
1925
+ message: "You must be 18 or older"
1528
1926
 
1529
1927
  }
1530
1928
  ```
@@ -1571,21 +1969,21 @@ When validation succeeds, `validatedPayload` is automatically added:
1571
1969
 
1572
1970
  {
1573
1971
 
1574
-   statusCode: 201,
1972
+ statusCode: 201,
1575
1973
 
1576
-   valid: true,
1974
+ valid: true,
1577
1975
 
1578
-   message: "Payload validated successfully",
1976
+ message: "Payload validated successfully",
1579
1977
 
1580
-   validatedPayload: {
1978
+ validatedPayload: {
1581
1979
 
1582
-     name: "Kiran",
1980
+ name: "Kiran",
1583
1981
 
1584
-     email: "kiran@example.com",
1982
+ email: "kiran@example.com",
1585
1983
 
1586
-     age: 29
1984
+ age: 29
1587
1985
 
1588
-   }
1986
+ }
1589
1987
 
1590
1988
  }
1591
1989
  ```
@@ -1620,27 +2018,27 @@ When validation fails, `errors` is automatically added:
1620
2018
 
1621
2019
  {
1622
2020
 
1623
-   statusCode: 422,
2021
+ statusCode: 422,
1624
2022
 
1625
-   valid: false,
2023
+ valid: false,
1626
2024
 
1627
-   message: "Payload validation failed",
2025
+ message: "Payload validation failed",
1628
2026
 
1629
-   errors: [
2027
+ errors: [
1630
2028
 
1631
-     {
2029
+ {
1632
2030
 
1633
-       path: "email",
2031
+ path: "email",
1634
2032
 
1635
-       code: "INVALID_EMAIL",
2033
+ code: "INVALID_EMAIL",
1636
2034
 
1637
-       message:
2035
+ message:
1638
2036
 
1639
-         "Invalid email format for attribute email"
2037
+ "Invalid email format for attribute email"
1640
2038
 
1641
-     }
2039
+ }
1642
2040
 
1643
-   ]
2041
+ ]
1644
2042
 
1645
2043
  }
1646
2044
  ```
@@ -1703,15 +2101,15 @@ is:
1703
2101
 
1704
2102
  {
1705
2103
 
1706
-   statusCode: 200,
2104
+ statusCode: 200,
1707
2105
 
1708
-   valid: true,
2106
+ valid: true,
1709
2107
 
1710
-   validatedPayload: {
2108
+ validatedPayload: {
1711
2109
 
1712
-     **// validated fields**
2110
+ ***// validated fields***
1713
2111
 
1714
-   }
2112
+ }
1715
2113
 
1716
2114
  }
1717
2115
  ```
@@ -1722,25 +2120,25 @@ The default invalid response is:
1722
2120
 
1723
2121
  {
1724
2122
 
1725
-   statusCode: 400,
2123
+ statusCode: 400,
1726
2124
 
1727
-   valid: false,
2125
+ valid: false,
1728
2126
 
1729
-   message: "One or more attribute values are invalid",
2127
+ message: "One or more attribute values are invalid",
1730
2128
 
1731
-   errors: [
2129
+ errors: [
1732
2130
 
1733
-     {
2131
+ {
1734
2132
 
1735
-       path: "field",
2133
+ path: "field",
1736
2134
 
1737
-       code: "ERROR_CODE",
2135
+ code: "ERROR_CODE",
1738
2136
 
1739
-       message: "Validation error message"
2137
+ message: "Validation error message"
1740
2138
 
1741
-     }
2139
+ }
1742
2140
 
1743
-   ]
2141
+ ]
1744
2142
 
1745
2143
  }
1746
2144
  ```
@@ -1771,11 +2169,11 @@ An error can be returned as:
1771
2169
 
1772
2170
  {
1773
2171
 
1774
-   path: "email",
2172
+ path: "email",
1775
2173
 
1776
-   code: "INVALID_EMAIL",
2174
+ code: "INVALID_EMAIL",
1777
2175
 
1778
-   message: "Invalid email format for attribute email"
2176
+ message: "Invalid email format for attribute email"
1779
2177
 
1780
2178
  }
1781
2179
  ```
@@ -1834,13 +2232,13 @@ its complete nested path:
1834
2232
 
1835
2233
  {
1836
2234
 
1837
-   path: "address.location.latitude",
2235
+ path: "address.location.latitude",
1838
2236
 
1839
-   code: "INVALID_TYPE",
2237
+ code: "INVALID_TYPE",
1840
2238
 
1841
-   message:
2239
+ message:
1842
2240
 
1843
-     "Invalid type for attribute address.location.latitude, required number value"
2241
+ "Invalid type for attribute address.location.latitude, required number value"
1844
2242
 
1845
2243
  }
1846
2244
  ```
@@ -1888,13 +2286,13 @@ The invalid third element is reported as:
1888
2286
 
1889
2287
  {
1890
2288
 
1891
-   path: "marks[2]",
2289
+ path: "marks[2]",
1892
2290
 
1893
-   code: "OUT_OF_RANGE",
2291
+ code: "OUT_OF_RANGE",
1894
2292
 
1895
-   message:
2293
+ message:
1896
2294
 
1897
-     "Attribute marks[2] should have a value between 0 and 100"
2295
+ "Attribute marks[2] should have a value between 0 and 100"
1898
2296
 
1899
2297
  }
1900
2298
  ```
@@ -1910,7 +2308,9 @@ marks[1]
1910
2308
  marks[2]
1911
2309
  ```
1912
2310
 
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`.
2311
+ Array-level constraints such as `minItems` and `maxItems` report the
2312
+ path of the array itself. For nested arrays, the complete parent path is
2313
+ retained, for example `orders[1].items`.
1914
2314
 
1915
2315
  ### Nested Fields Inside Arrays
1916
2316
 
@@ -1969,11 +2369,11 @@ Result:
1969
2369
 
1970
2370
  {
1971
2371
 
1972
-   "email": "Invalid email format for attribute email",
2372
+ "email": "Invalid email format for attribute email",
1973
2373
 
1974
-   "address.location.latitude": "Invalid type for attribute address.location.latitude, required number value",
2374
+ "address.location.latitude": "Invalid type for attribute address.location.latitude, required number value",
1975
2375
 
1976
-   "marks[2]": "Attribute marks[2] should have a value between 0 and 100"
2376
+ "marks[2]": "Attribute marks[2] should have a value between 0 and 100"
1977
2377
 
1978
2378
  }
1979
2379
  ```
@@ -1988,61 +2388,61 @@ sample-1
1988
2388
 
1989
2389
  {
1990
2390
 
1991
-   firstName: {
2391
+ firstName: {
1992
2392
 
1993
-     mandatory: true,
2393
+ mandatory: true,
1994
2394
 
1995
-     allowNull: false,
2395
+ allowNull: false,
1996
2396
 
1997
-     type: "string",
2397
+ type: "string",
1998
2398
 
1999
-     minLength: 3,
2399
+ minLength: 3,
2000
2400
 
2001
-     minLengthError: "First name must have minimum 3 characters."
2401
+ minLengthError: "First name must have minimum 3 characters."
2002
2402
 
2003
-   },
2403
+ },
2004
2404
 
2005
-   lastName: {
2405
+ lastName: {
2006
2406
 
2007
-     mandatory: false,
2407
+ mandatory: false,
2008
2408
 
2009
-     allowNull: true,
2409
+ allowNull: true,
2010
2410
 
2011
-     type: "string",
2411
+ type: "string",
2012
2412
 
2013
-   },
2413
+ },
2014
2414
 
2015
-   email: {
2415
+ email: {
2016
2416
 
2017
-     mandatory: true,
2417
+ mandatory: true,
2018
2418
 
2019
-     allowNull: false,
2419
+ allowNull: false,
2020
2420
 
2021
-     type: "email",
2421
+ type: "email",
2022
2422
 
2023
-   },
2423
+ },
2024
2424
 
2025
-   phone: {
2425
+ phone: {
2026
2426
 
2027
-     mandatory: true,
2427
+ mandatory: true,
2028
2428
 
2029
-     allowNull: false,
2429
+ allowNull: false,
2030
2430
 
2031
-     type: "string",
2431
+ type: "string",
2032
2432
 
2033
-   },
2433
+ },
2034
2434
 
2035
-   age: {
2435
+ age: {
2036
2436
 
2037
-     mandatory: false,
2437
+ mandatory: false,
2038
2438
 
2039
-     type: "number",
2439
+ type: "number",
2040
2440
 
2041
-     min: 1,
2441
+ min: 1,
2042
2442
 
2043
-     max: 120,
2443
+ max: 120,
2044
2444
 
2045
-   },
2445
+ },
2046
2446
 
2047
2447
  };
2048
2448
  ```
@@ -2053,271 +2453,271 @@ sample-2
2053
2453
 
2054
2454
  {
2055
2455
 
2056
-   id: {
2456
+ id: {
2057
2457
 
2058
-     mandatory: true,
2458
+ mandatory: true,
2059
2459
 
2060
-     allowNull: true,
2460
+ allowNull: true,
2061
2461
 
2062
-     type: "uuidv4",
2462
+ type: "uuidv4",
2063
2463
 
2064
-   },
2464
+ },
2065
2465
 
2066
-   batchId: {
2466
+ batchId: {
2067
2467
 
2068
-     mandatory: true,
2468
+ mandatory: true,
2069
2469
 
2070
-     allowNull: true,
2470
+ allowNull: true,
2071
2471
 
2072
-     type: "objectId",
2472
+ type: "objectId",
2073
2473
 
2074
-   },
2474
+ },
2075
2475
 
2076
-   firstName: {
2476
+ firstName: {
2077
2477
 
2078
-     mandatory: true,
2478
+ mandatory: true,
2079
2479
 
2080
-     type: "string",
2480
+ type: "string",
2081
2481
 
2082
-     minLength: 3,
2482
+ minLength: 3,
2083
2483
 
2084
-   },
2484
+ },
2085
2485
 
2086
-   lastName: {
2486
+ lastName: {
2087
2487
 
2088
-     mandatory: false,
2488
+ mandatory: false,
2089
2489
 
2090
-     allowNull: true,
2490
+ allowNull: true,
2091
2491
 
2092
-     type: "string",
2492
+ type: "string",
2093
2493
 
2094
-   },
2494
+ },
2095
2495
 
2096
-   age: {
2496
+ age: {
2097
2497
 
2098
-     type: "number",
2498
+ type: "number",
2099
2499
 
2100
-     min: 0.1,
2500
+ min: 0.1,
2101
2501
 
2102
-     max: 120,
2502
+ max: 120,
2103
2503
 
2104
-   },
2504
+ },
2105
2505
 
2106
-   isAdult: {
2506
+ isAdult: {
2107
2507
 
2108
-     type: "boolean",
2508
+ type: "boolean",
2109
2509
 
2110
-   },
2510
+ },
2111
2511
 
2112
-   totalWins: {
2512
+ totalWins: {
2113
2513
 
2114
-     type: "number",
2514
+ type: "number",
2115
2515
 
2116
-     min: 0,
2516
+ min: 0,
2117
2517
 
2118
-     preventDecimal: true,
2518
+ preventDecimal: true,
2119
2519
 
2120
-   },
2520
+ },
2121
2521
 
2122
-   email: {
2522
+ email: {
2123
2523
 
2124
-     regex: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$/,
2524
+ regex: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\\\\\.[a-zA-Z]{2,}$/,
2125
2525
 
2126
-   },
2526
+ },
2127
2527
 
2128
-   githubLink: {
2528
+ githubLink: {
2129
2529
 
2130
-     type: "url",
2530
+ type: "url",
2131
2531
 
2132
-   },
2532
+ },
2133
2533
 
2134
-   accountStatus: {
2534
+ accountStatus: {
2135
2535
 
2136
-     type: "enum",
2536
+ type: "enum",
2137
2537
 
2138
-     enumValues: ["Active", "Inactive", 200],
2538
+ enumValues: ["Active", "Inactive", 200],
2139
2539
 
2140
-   },
2540
+ },
2141
2541
 
2142
-   marks: {
2542
+ marks: {
2143
2543
 
2144
-     range: "0-100",
2544
+ range: "0-100",
2145
2545
 
2146
-   },
2546
+ },
2147
2547
 
2148
-   allMarks: {
2548
+ allMarks: {
2149
2549
 
2150
-     type: "array",
2550
+ type: "array",
2151
2551
 
2152
-     allowEmptyArray: false,
2552
+ allowEmptyArray: false,
2153
2553
 
2154
-     elementConstraints: {
2554
+ elementConstraints: {
2155
2555
 
2156
-       type: "number",
2556
+ type: "number",
2157
2557
 
2158
-       allowNull: false,
2558
+ allowNull: false,
2159
2559
 
2160
-       range: "0-100",
2560
+ range: "0-100",
2161
2561
 
2162
-     },
2562
+ },
2163
2563
 
2164
-   },
2564
+ },
2165
2565
 
2166
-   totalScore: {
2566
+ totalScore: {
2167
2567
 
2168
-     type: "number",
2568
+ type: "number",
2169
2569
 
2170
-     dependency: {
2570
+ dependency: {
2171
2571
 
2172
-       result: {
2572
+ result: {
2173
2573
 
2174
-         setDependencyRule: (totalScore, result) => {
2574
+ setDependencyRule: (totalScore, result) => {
2175
2575
 
2176
-           return { mandatory: true, allowNull: false, type: "string" };
2576
+ return { mandatory: true, allowNull: false, type: "string" };
2177
2577
 
2178
-         },
2578
+ },
2179
2579
 
2180
-       },
2580
+ },
2181
2581
 
2182
-     },
2582
+ },
2183
2583
 
2184
-   },
2584
+ },
2185
2585
 
2186
-   result: {
2586
+ result: {
2187
2587
 
2188
-     type: "string",
2588
+ type: "string",
2189
2589
 
2190
-     dependency: {
2590
+ dependency: {
2191
2591
 
2192
-       totalScore: {
2592
+ totalScore: {
2193
2593
 
2194
-         setDependencyRule: (result, totalScore) => {
2594
+ setDependencyRule: (result, totalScore) => {
2195
2595
 
2196
-           return { mandatory: true, allowNull: false, type: "number" };
2596
+ return { mandatory: true, allowNull: false, type: "number" };
2197
2597
 
2198
-         },
2598
+ },
2199
2599
 
2200
-       },
2600
+ },
2201
2601
 
2202
-     },
2602
+ },
2203
2603
 
2204
-   },
2604
+ },
2205
2605
 
2206
-   minSalary: {
2606
+ minSalary: {
2207
2607
 
2208
-     mandatory: true,
2608
+ mandatory: true,
2209
2609
 
2210
-     min: 1,
2610
+ min: 1,
2211
2611
 
2212
-     type: "number",
2612
+ type: "number",
2213
2613
 
2214
-     dependency: {
2614
+ dependency: {
2215
2615
 
2216
-       maxSalary: {
2616
+ maxSalary: {
2217
2617
 
2218
-         setDependencyRule: (minSalary, maxSalary) => {
2618
+ setDependencyRule: (minSalary, maxSalary) => {
2219
2619
 
2220
-           return {
2620
+ return {
2221
2621
 
2222
-             mandatory: true,
2622
+ mandatory: true,
2223
2623
 
2224
-             min: minSalary + 1,
2624
+ min: minSalary + 1,
2225
2625
 
2226
-             minError: "maxSalary must be more than minSalary",
2626
+ minError: "maxSalary must be more than minSalary",
2227
2627
 
2228
-           };
2628
+ };
2229
2629
 
2230
-         },
2630
+ },
2231
2631
 
2232
-       },
2632
+ },
2233
2633
 
2234
-     },
2634
+ },
2235
2635
 
2236
-   },
2636
+ },
2237
2637
 
2238
-   maxSalary: {
2638
+ maxSalary: {
2239
2639
 
2240
-     dependency: {
2640
+ dependency: {
2241
2641
 
2242
-       minSalary: {
2642
+ minSalary: {
2243
2643
 
2244
-         setDependencyRule: (maxSalary, minSalary) => {
2644
+ setDependencyRule: (maxSalary, minSalary) => {
2245
2645
 
2246
-           return {
2646
+ return {
2247
2647
 
2248
-             mandatory: true,
2648
+ mandatory: true,
2249
2649
 
2250
-             max: maxSalary - 1,
2650
+ max: maxSalary - 1,
2251
2651
 
2252
-             maxError: "minSalary must be less than maxSalary",
2652
+ maxError: "minSalary must be less than maxSalary",
2253
2653
 
2254
-           };
2654
+ };
2255
2655
 
2256
-         },
2656
+ },
2257
2657
 
2258
-       },
2658
+ },
2259
2659
 
2260
-     },
2660
+ },
2261
2661
 
2262
-   },
2662
+ },
2263
2663
 
2264
-   address: {
2664
+ address: {
2265
2665
 
2266
-     mandatory: true,
2666
+ mandatory: true,
2267
2667
 
2268
-     type: "object",
2668
+ type: "object",
2269
2669
 
2270
-     allowEmptyObject: false,
2670
+ allowEmptyObject: false,
2271
2671
 
2272
-     objectAttr: {
2672
+ objectAttr: {
2273
2673
 
2274
-       country: { mandatory: true, type: "string" },
2674
+ country: { mandatory: true, type: "string" },
2275
2675
 
2276
-       state: {
2676
+ state: {
2277
2677
 
2278
-         mandatory: true,
2678
+ mandatory: true,
2279
2679
 
2280
-         type: "string",
2680
+ type: "string",
2281
2681
 
2282
-       },
2682
+ },
2283
2683
 
2284
-       city: {},
2684
+ city: {},
2285
2685
 
2286
-       zip: {
2686
+ zip: {
2287
2687
 
2288
-         mandatory: true,
2688
+ mandatory: true,
2289
2689
 
2290
-         type: "string",
2690
+ type: "string",
2291
2691
 
2292
-       },
2692
+ },
2293
2693
 
2294
-       position: {
2694
+ position: {
2295
2695
 
2296
-         mandatory: true,
2696
+ mandatory: true,
2297
2697
 
2298
-         type: "object",
2698
+ type: "object",
2299
2699
 
2300
-         allowEmptyObject: false,
2700
+ allowEmptyObject: false,
2301
2701
 
2302
-         objectAttr: {
2702
+ objectAttr: {
2303
2703
 
2304
-           lattitude: { mandatory: true, type: "number" },
2704
+ lattitude: { mandatory: true, type: "number" },
2305
2705
 
2306
-           longitude: {
2706
+ longitude: {
2307
2707
 
2308
-             mandatory: true,
2708
+ mandatory: true,
2309
2709
 
2310
-             type: "number",
2710
+ type: "number",
2311
2711
 
2312
-           },
2712
+ },
2313
2713
 
2314
-         },
2714
+ },
2315
2715
 
2316
-       },
2716
+ },
2317
2717
 
2318
-     },
2718
+ },
2319
2719
 
2320
-   },
2720
+ },
2321
2721
 
2322
2722
  }
2323
2723
  ```
@@ -2328,15 +2728,15 @@ sample-2
2328
2728
 
2329
2729
  ```js
2330
2730
 
2331
- **// validatePayload is the middleware that invokes perfectPayload()**
2731
+ ***// validatePayload is the middleware that invokes perfectPayload()***
2332
2732
 
2333
2733
  router.post(
2334
2734
 
2335
-   "/payload-validation",
2735
+ "/payload-validation",
2336
2736
 
2337
-   validatePayload({ rule: <your validation rule json object> }),
2737
+ validatePayload({ rule: <your validation rule json object> }),
2338
2738
 
2339
-   (req, res) => res.send("OK")
2739
+ (req, res) => res.send("OK")
2340
2740
 
2341
2741
  );
2342
2742
  ```
@@ -2365,6 +2765,47 @@ export const validatePayload = ({ rule }) => {
2365
2765
  };
2366
2766
  ```
2367
2767
 
2768
+ #### Async ES Modules middleware example
2769
+
2770
+ When your schema contains an asynchronous `customValidator`, the
2771
+ middleware itself must be `async` and `perfectPayloadAsync()` must be
2772
+ awaited:
2773
+
2774
+ ```js
2775
+ import { perfectPayloadAsync } from "perfect-payload";
2776
+
2777
+ export const validatePayloadAsync = ({ rule }) => {
2778
+ return async (req, res, next) => {
2779
+ try {
2780
+ const { statusCode, ...response } = await perfectPayloadAsync(
2781
+ req?.body,
2782
+ rule,
2783
+ );
2784
+
2785
+ if (+statusCode >= 200 && +statusCode <= 299) {
2786
+ req.validatedBody = response?.validatedPayload;
2787
+ next();
2788
+ } else {
2789
+ res.status(statusCode).json(response);
2790
+ }
2791
+ } catch (error) {
2792
+ console.error("Error validating payload", error);
2793
+ res.status(500).json({ error: "Internal Server Error" });
2794
+ }
2795
+ };
2796
+ };
2797
+ ```
2798
+
2799
+ Route usage:
2800
+
2801
+ ```js
2802
+ router.post(
2803
+ "/payload-validation",
2804
+ validatePayloadAsync({ rule: <your validation rule json object> }),
2805
+ (req, res) => res.send("OK"),
2806
+ );
2807
+ ```
2808
+
2368
2809
  #### CommonJS middleware example
2369
2810
 
2370
2811
  ```js