perfect-payload 1.6.0-beta.0 → 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 +501 -99
  2. package/index.js +81 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,13 +24,16 @@ lightweight.
24
24
  `perfectPayloadAsync()`
25
25
  - Transformed values returned through `validatedPayload`
26
26
  - Original input payload is not mutated
27
- - Extra payload fields are filtered from `validatedPayload`
27
+ - Configurable unknown-field handling: `strip`, `allow`, or `reject`
28
+ - Clean three-argument API with validation options in one object
28
29
  - Legacy `perfectPayloadV1()` retained during the migration period
29
30
 
30
31
  ## Quick Links
31
32
 
32
33
  - [Installation](#installation)
33
34
  - [Basic Usage](#basic-usage)
35
+ - [Public API](#public-api)
36
+ - [Unknown Field Handling](#unknown-field-handling)
34
37
  - [Synchronous vs Asynchronous
35
38
  Validation](#synchronous-vs-asynchronous-validation)
36
39
  - [Validation Rules](#validation-rules)
@@ -47,6 +50,31 @@ lightweight.
47
50
  - [Examples and Usage](#examples-and-usage)
48
51
  - [Legacy API](#legacy-api)
49
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
+
50
78
  ## Installation
51
79
 
52
80
  ```bash
@@ -119,13 +147,11 @@ console.log(result);
119
147
  }
120
148
  ```
121
149
 
122
- Note: The validatedPayload contains only the fields
123
-
124
- defined in the
125
-
126
- 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"`.
127
153
 
128
- to safely overwrite request.body or assign it to a new request property
154
+ The original input payload is not mutated.
129
155
 
130
156
  (such as validatedBody, sanitisedData or parsedBody).
131
157
 
@@ -183,6 +209,375 @@ failure.
183
209
 
184
210
  \- Submitted payload values are not included in default error messages.
185
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
+
186
581
  ## Synchronous vs Asynchronous Validation
187
582
 
188
583
  For normal synchronous validation, use `perfectPayload()`:
@@ -190,7 +585,7 @@ For normal synchronous validation, use `perfectPayload()`:
190
585
  ```js
191
586
  import { perfectPayload } from "perfect-payload";
192
587
 
193
- const result = perfectPayload(payload, validationRules);
588
+ const result = perfectPayload(payload, validationRules, options);
194
589
  ```
195
590
 
196
591
  When any `customValidator` needs to perform asynchronous work, use
@@ -199,15 +594,15 @@ When any `customValidator` needs to perform asynchronous work, use
199
594
  ```js
200
595
  import { perfectPayloadAsync } from "perfect-payload";
201
596
 
202
- const result = await perfectPayloadAsync(payload, validationRules);
597
+ const result = await perfectPayloadAsync(payload, validationRules, options);
203
598
  ```
204
599
 
205
600
  The public APIs are:
206
601
 
207
602
  ```text
208
- perfectPayloadV1() legacy API; deprecated
209
- perfectPayload() synchronous validation
210
- perfectPayloadAsync() synchronous + asynchronous customValidator
603
+ perfectPayloadV1() legacy API; deprecated
604
+ perfectPayload(data, rules, options?) synchronous validation
605
+ perfectPayloadAsync(data, rules, options?) synchronous + asynchronous customValidator
211
606
  ```
212
607
 
213
608
  `perfectPayload()` remains synchronous and intentionally rejects a
@@ -246,10 +641,16 @@ March 31, 2027.
246
641
 
247
642
  Existing applications can continue using it during the migration period,
248
643
 
249
- but all new implementations should use:
644
+ but all new implementations should use the current API:
250
645
 
251
646
  ```js
252
- perfectPayload();
647
+ perfectPayload(data, validationRules, options?);
648
+ ```
649
+
650
+ For asynchronous custom validation:
651
+
652
+ ```js
653
+ await perfectPayloadAsync(data, validationRules, options?);
253
654
  ```
254
655
 
255
656
  The legacy API continues to return validation errors as:
@@ -1661,6 +2062,8 @@ MAX_VALUE
1661
2062
  OUT_OF_RANGE
1662
2063
 
1663
2064
  CUSTOM_VALIDATION_FAILED
2065
+
2066
+ UNKNOWN_FIELD
1664
2067
  ```
1665
2068
 
1666
2069
  These codes are designed for programmatic handling while `message`
@@ -1939,157 +2342,156 @@ handling
1939
2342
 
1940
2343
  ## Custom Response Objects
1941
2344
 
1942
- `perfectPayload()` allows you to customize both the valid and invalid
2345
+ Custom valid and invalid response objects are configured inside the
2346
+ optional third `options` argument.
1943
2347
 
1944
- response objects.
1945
-
1946
- The third argument is the custom valid response.
2348
+ ```js
2349
+ perfectPayload(data, validationRules, options?)
2350
+ await perfectPayloadAsync(data, validationRules, options?)
2351
+ ```
1947
2352
 
1948
- The fourth argument is the custom invalid response.
2353
+ This keeps API-level configuration in one place and avoids positional
2354
+ `undefined` arguments.
1949
2355
 
1950
2356
  ### Custom Valid Response
1951
2357
 
1952
- Example:
1953
-
1954
2358
  ```js
1955
- const customValidResponse = {
1956
- statusCode: 201,
1957
-
1958
- valid: true,
1959
-
1960
- message: "Payload validated successfully",
1961
- };
1962
-
1963
- 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
+ });
1964
2366
  ```
1965
2367
 
1966
2368
  When validation succeeds, `validatedPayload` is automatically added:
1967
2369
 
1968
2370
  ```js
1969
-
1970
2371
  {
1971
-
1972
2372
  statusCode: 201,
1973
-
1974
2373
  valid: true,
1975
-
1976
2374
  message: "Payload validated successfully",
1977
-
1978
2375
  validatedPayload: {
1979
-
1980
2376
  name: "Kiran",
1981
-
1982
2377
  email: "kiran@example.com",
1983
-
1984
2378
  age: 29
1985
-
1986
2379
  }
1987
-
1988
2380
  }
1989
2381
  ```
1990
2382
 
1991
2383
  ### Custom Invalid Response
1992
2384
 
1993
- Example:
1994
-
1995
2385
  ```js
1996
- const customInvalidResponse = {
1997
- statusCode: 422,
1998
-
1999
- valid: false,
2000
-
2001
- message: "Payload validation failed",
2002
- };
2003
-
2004
- const result = perfectPayload(
2005
- payload,
2006
-
2007
- validationRules,
2008
-
2009
- undefined,
2010
-
2011
- customInvalidResponse,
2012
- );
2386
+ const result = perfectPayload(payload, validationRules, {
2387
+ inValidPayloadResponse: {
2388
+ statusCode: 422,
2389
+ valid: false,
2390
+ message: "Payload validation failed",
2391
+ },
2392
+ });
2013
2393
  ```
2014
2394
 
2015
2395
  When validation fails, `errors` is automatically added:
2016
2396
 
2017
2397
  ```js
2018
-
2019
2398
  {
2020
-
2021
2399
  statusCode: 422,
2022
-
2023
2400
  valid: false,
2024
-
2025
2401
  message: "Payload validation failed",
2026
-
2027
2402
  errors: [
2028
-
2029
2403
  {
2030
-
2031
2404
  path: "email",
2032
-
2033
2405
  code: "INVALID_EMAIL",
2034
-
2035
- message:
2036
-
2037
- "Invalid email format for attribute email"
2038
-
2406
+ message: "Invalid email format for attribute email"
2039
2407
  }
2040
-
2041
2408
  ]
2042
-
2043
2409
  }
2044
2410
  ```
2045
2411
 
2046
2412
  ### Custom Valid and Invalid Responses Together
2047
2413
 
2048
2414
  ```js
2049
- const customValidResponse = {
2050
- statusCode: 201,
2415
+ const result = perfectPayload(payload, validationRules, {
2416
+ validPayloadResponse: {
2417
+ statusCode: 201,
2418
+ valid: true,
2419
+ message: "CUSTOM_VALID_RESPONSE",
2420
+ },
2051
2421
 
2052
- valid: true,
2422
+ inValidPayloadResponse: {
2423
+ statusCode: 422,
2424
+ valid: false,
2425
+ message: "CUSTOM_INVALID_RESPONSE",
2426
+ },
2427
+ });
2428
+ ```
2053
2429
 
2054
- message: "CUSTOM_VALID_RESPONSE",
2055
- };
2430
+ You can combine response customization with other API options:
2056
2431
 
2057
- const customInvalidResponse = {
2058
- statusCode: 422,
2432
+ ```js
2433
+ const result = perfectPayload(payload, validationRules, {
2434
+ unknownFields: "reject",
2059
2435
 
2060
- valid: false,
2436
+ validPayloadResponse: {
2437
+ statusCode: 201,
2438
+ valid: true,
2439
+ },
2061
2440
 
2062
- message: "CUSTOM_INVALID_RESPONSE",
2063
- };
2441
+ inValidPayloadResponse: {
2442
+ statusCode: 422,
2443
+ valid: false,
2444
+ message: "Payload validation failed",
2445
+ },
2446
+ });
2447
+ ```
2064
2448
 
2065
- const result = perfectPayload(
2066
- payload,
2449
+ The response object you provide is preserved while `perfectPayload()`
2450
+ automatically adds `validatedPayload` for successful validation or
2451
+ `errors` for failed validation.
2067
2452
 
2068
- validationRules,
2453
+ The same response options are supported by `perfectPayloadAsync()`.
2069
2454
 
2070
- customValidResponse,
2455
+ ## v1.7 API Migration
2071
2456
 
2072
- customInvalidResponse,
2073
- );
2074
- ```
2457
+ The current `perfectPayload()` and `perfectPayloadAsync()` APIs use one
2458
+ optional third argument for configuration:
2075
2459
 
2076
- The response object you provide is preserved, while `perfectPayload()`
2460
+ ```js
2461
+ perfectPayload(data, validationRules, options?)
2462
+ perfectPayloadAsync(data, validationRules, options?)
2463
+ ```
2077
2464
 
2078
- automatically adds either:
2465
+ Custom response objects now belong inside `options`.
2079
2466
 
2080
- ```text
2467
+ Use:
2081
2468
 
2082
- validatedPayload
2469
+ ```js
2470
+ perfectPayload(payload, rules, {
2471
+ validPayloadResponse: customValidResponse,
2472
+ inValidPayloadResponse: customInvalidResponse,
2473
+ });
2083
2474
  ```
2084
2475
 
2085
- for successful validation, or:
2476
+ instead of passing custom response objects as separate positional
2477
+ arguments.
2086
2478
 
2087
- ```text
2479
+ This also makes it possible to combine response customization with
2480
+ `unknownFields` without placeholder arguments:
2088
2481
 
2089
- 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
+ });
2090
2491
  ```
2091
2492
 
2092
- for failed validation.
2493
+ `perfectPayloadV1()` is unchanged and retains its legacy signature
2494
+ during its deprecation period.
2093
2495
 
2094
2496
  ## Default Responses
2095
2497
 
package/index.js CHANGED
@@ -522,31 +522,66 @@ export function perfectPayloadV1(
522
522
  export function perfectPayload(
523
523
  data = {},
524
524
  dataValidationRule = {},
525
- validPayloadResponse = { statusCode: 200, valid: true },
526
- inValidPayloadResponse = {
527
- statusCode: 400,
528
- valid: false,
529
- message: "One or more attribute values are invalid",
530
- },
525
+ options = {},
531
526
  ) {
527
+ const {
528
+ unknownFields = "strip",
529
+
530
+ validPayloadResponse = {
531
+ statusCode: 200,
532
+ valid: true,
533
+ },
534
+
535
+ inValidPayloadResponse = {
536
+ statusCode: 400,
537
+ valid: false,
538
+ message: "One or more attribute values are invalid",
539
+ },
540
+ } = options ?? {};
541
+
542
+ if (!["strip", "allow", "reject"].includes(unknownFields)) {
543
+ throw new Error(
544
+ `perfect-payload:- unknownFields must be one of strip, allow, reject`,
545
+ );
546
+ }
547
+
532
548
  return perfectPayloadStructured(
533
549
  data,
534
550
  dataValidationRule,
535
551
  validPayloadResponse,
536
552
  inValidPayloadResponse,
553
+ "",
554
+ {
555
+ unknownFields,
556
+ },
537
557
  );
538
558
  }
539
559
 
540
560
  export async function perfectPayloadAsync(
541
561
  data = {},
542
562
  dataValidationRule = {},
543
- validPayloadResponse = { statusCode: 200, valid: true },
544
- inValidPayloadResponse = {
545
- statusCode: 400,
546
- valid: false,
547
- message: "One or more attribute values are invalid",
548
- },
563
+ options = {},
549
564
  ) {
565
+ const {
566
+ validPayloadResponse = {
567
+ statusCode: 200,
568
+ valid: true,
569
+ },
570
+
571
+ inValidPayloadResponse = {
572
+ statusCode: 400,
573
+ valid: false,
574
+ message: "One or more attribute values are invalid",
575
+ },
576
+ unknownFields = "strip",
577
+ } = options ?? {};
578
+
579
+ if (!["strip", "allow", "reject"].includes(unknownFields)) {
580
+ throw new Error(
581
+ `perfect-payload:- unknownFields must be one of strip, allow, reject`,
582
+ );
583
+ }
584
+
550
585
  const validationResult = perfectPayloadStructured(
551
586
  data,
552
587
  dataValidationRule,
@@ -555,6 +590,7 @@ export async function perfectPayloadAsync(
555
590
  "",
556
591
  {
557
592
  skipCustomValidator: true,
593
+ unknownFields,
558
594
  },
559
595
  );
560
596
 
@@ -566,6 +602,7 @@ export async function perfectPayloadAsync(
566
602
  validationResult?.validatedPayload ?? {},
567
603
  dataValidationRule,
568
604
  );
605
+
569
606
  if (rowErrors.length > 0) {
570
607
  return {
571
608
  ...inValidPayloadResponse,
@@ -591,6 +628,7 @@ function perfectPayloadStructured(
591
628
  let validatedPayload = {};
592
629
  let rowErrors = [];
593
630
  const skipCustomValidator = options?.skipCustomValidator === true;
631
+ const unknownFields = options?.unknownFields ?? "strip";
594
632
 
595
633
  for (const attributeName in dataValidationRule) {
596
634
  let addNextError = true;
@@ -1520,6 +1558,36 @@ function perfectPayloadStructured(
1520
1558
  }
1521
1559
  }
1522
1560
 
1561
+ // UNKNOWN FIELD HANDLING
1562
+ if (unknownFields === "allow") {
1563
+ for (const attributeName in data) {
1564
+ if (
1565
+ Object.prototype.hasOwnProperty.call(data, attributeName) &&
1566
+ !Object.prototype.hasOwnProperty.call(dataValidationRule, attributeName)
1567
+ ) {
1568
+ validatedPayload[attributeName] = data[attributeName];
1569
+ }
1570
+ }
1571
+ }
1572
+
1573
+ if (unknownFields === "reject") {
1574
+ for (const attributeName in data) {
1575
+ if (
1576
+ Object.prototype.hasOwnProperty.call(data, attributeName) &&
1577
+ !Object.prototype.hasOwnProperty.call(dataValidationRule, attributeName)
1578
+ ) {
1579
+ const path = basePath ? `${basePath}.${attributeName}` : attributeName;
1580
+
1581
+ rowErrors.push({
1582
+ path,
1583
+ code: "UNKNOWN_FIELD",
1584
+ message: `Unknown field ${path} is not allowed`,
1585
+ });
1586
+ }
1587
+ }
1588
+ }
1589
+
1590
+ // INVALID RESPONSE
1523
1591
  if (rowErrors.length > 0) {
1524
1592
  return {
1525
1593
  ...inValidPayloadResponse,
@@ -1527,6 +1595,7 @@ function perfectPayloadStructured(
1527
1595
  };
1528
1596
  }
1529
1597
 
1598
+ // VALID RESPONSE
1530
1599
  return {
1531
1600
  ...validPayloadResponse,
1532
1601
  validatedPayload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perfect-payload",
3
- "version": "1.6.0-beta.0",
3
+ "version": "1.7.0-beta.0",
4
4
  "type": "module",
5
5
  "description": "Lightweight JSON payload validation library with structured errors, nested validation, field paths, and customizable validation rules.",
6
6
  "main": "index.js",