zod-validate 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bibek Bhattarai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,833 @@
1
+ # zod-validate
2
+
3
+ Reusable validation utilities built on top of [Zod](https://zod.dev/) for frontend and backend applications.
4
+
5
+ `zod-validate` provides a small layer around Zod's `safeParse()` API so you can create a reusable validator from a schema and use the same validator for:
6
+
7
+ - Whole-form validation
8
+ - Individual field validation
9
+ - Nested object validation
10
+ - Nested array validation
11
+ - Validation error codes
12
+ - Human-readable message resolution
13
+ - Localized validation messages
14
+
15
+ The package does **not** replace Zod. Zod remains the validation engine.
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install zod-validate zod
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Why zod-validate?
28
+
29
+ Zod already provides excellent schema validation.
30
+
31
+ The problem is usually what happens **around** the schema.
32
+
33
+ For example, an application may need to:
34
+
35
+ - Validate an entire form.
36
+ - Validate one field while the user is typing.
37
+ - Keep validation errors associated with their original fields.
38
+ - Handle deeply nested objects and arrays.
39
+ - Return stable validation codes instead of UI-specific messages.
40
+ - Resolve those codes into different languages on the frontend.
41
+
42
+ `zod-validate` provides a small, consistent API for those tasks.
43
+
44
+ Instead of repeatedly working directly with Zod's `safeParse()` result, create a validator once:
45
+
46
+ ```ts
47
+ const signup = createValidator(signupSchema);
48
+ ```
49
+
50
+ Then use it wherever needed:
51
+
52
+ ```ts
53
+ signup.validate(data);
54
+ signup.validateField("mobile_no", value);
55
+ ```
56
+
57
+ ---
58
+
59
+ # Basic Usage
60
+
61
+ ## 1. Define a Zod schema
62
+
63
+ You continue to define your validation rules using Zod.
64
+
65
+ ```ts
66
+ import { z } from "zod";
67
+
68
+ const signupSchema = z.object({
69
+ mobile_no: z
70
+ .string("mobile_no")
71
+ .trim()
72
+ .min(1, "mobile_no_required")
73
+ .regex(/^[0-9]+$/, "mobile_no_digits")
74
+ .regex(/^(98|97)/, "mobile_no_prefix")
75
+ .length(10, "mobile_no_length"),
76
+
77
+ password: z.string("password").min(8, "password_min_length"),
78
+ });
79
+ ```
80
+
81
+ The validation codes are supplied as Zod messages:
82
+
83
+ ```ts
84
+ "mobile_no_required";
85
+ "mobile_no_digits";
86
+ "mobile_no_prefix";
87
+ "mobile_no_length";
88
+ "password_min_length";
89
+ ```
90
+
91
+ These codes can later be converted into human-readable messages.
92
+
93
+ ---
94
+
95
+ # 2. Create a Validator
96
+
97
+ Import `createValidator`:
98
+
99
+ ```ts
100
+ import { createValidator } from "zod-validate";
101
+ ```
102
+
103
+ Create a validator from your schema:
104
+
105
+ ```ts
106
+ const signup = createValidator(signupSchema);
107
+ ```
108
+
109
+ The validator is now tied to that schema.
110
+
111
+ ---
112
+
113
+ # 3. Validate an Entire Object
114
+
115
+ Pass your data to `validate()`:
116
+
117
+ ```ts
118
+ const result = signup.validate({
119
+ mobile_no: "9812345678",
120
+ password: "password123",
121
+ });
122
+ ```
123
+
124
+ If the data is valid:
125
+
126
+ ```ts
127
+ {
128
+ valid: true,
129
+ data: {
130
+ mobile_no: "9812345678",
131
+ password: "password123"
132
+ }
133
+ }
134
+ ```
135
+
136
+ The returned `data` is the data parsed by Zod.
137
+
138
+ This means Zod transformations are also preserved.
139
+
140
+ ---
141
+
142
+ ## Invalid data
143
+
144
+ ```ts
145
+ const result = signup.validate({
146
+ mobile_no: "",
147
+ password: "123",
148
+ });
149
+ ```
150
+
151
+ The result is:
152
+
153
+ ```ts
154
+ {
155
+ valid: false,
156
+ errors: {
157
+ mobile_no: "mobile_no_required",
158
+ password: "password_min_length"
159
+ }
160
+ }
161
+ ```
162
+
163
+ The validator returns the validation codes rather than forcing a human-readable message into your validation layer.
164
+
165
+ ---
166
+
167
+ # 4. Type-Safe Result Handling
168
+
169
+ The result is a discriminated union.
170
+
171
+ ```ts
172
+ const result = signup.validate(data);
173
+
174
+ if (result.valid) {
175
+ console.log(result.data);
176
+ } else {
177
+ console.log(result.errors);
178
+ }
179
+ ```
180
+
181
+ TypeScript understands that:
182
+
183
+ ```ts
184
+ result.valid === true;
185
+ ```
186
+
187
+ means `result.data` is available.
188
+
189
+ And:
190
+
191
+ ```ts
192
+ result.valid === false;
193
+ ```
194
+
195
+ means `result.errors` is available.
196
+
197
+ ---
198
+
199
+ # 5. Validate a Single Field
200
+
201
+ For frontend forms, you often don't want to validate the entire form.
202
+
203
+ For example, while the user is entering their mobile number:
204
+
205
+ ```ts
206
+ const result = signup.validateField("mobile_no", "9812345678");
207
+ ```
208
+
209
+ If valid:
210
+
211
+ ```ts
212
+ {
213
+ valid: true,
214
+ data: "9812345678"
215
+ }
216
+ ```
217
+
218
+ If invalid:
219
+
220
+ ```ts
221
+ const result = signup.validateField("mobile_no", "abc");
222
+ ```
223
+
224
+ You get:
225
+
226
+ ```ts
227
+ {
228
+ valid: false,
229
+ errors: {
230
+ mobile_no: "mobile_no_digits"
231
+ }
232
+ }
233
+ ```
234
+
235
+ This makes the same schema useful for both:
236
+
237
+ ```text
238
+ Whole form validation
239
+ +
240
+ Individual field validation
241
+ ```
242
+
243
+ ---
244
+
245
+ # 6. Why Field Validation Is Useful
246
+
247
+ A frontend form might use the validator like this:
248
+
249
+ ```ts
250
+ const result = signup.validateField("mobile_no", mobileNo);
251
+
252
+ if (!result.valid) {
253
+ setMobileError(result.errors.mobile_no);
254
+ }
255
+ ```
256
+
257
+ Then when the entire form is submitted:
258
+
259
+ ```ts
260
+ const result = signup.validate(formData);
261
+
262
+ if (!result.valid) {
263
+ setErrors(result.errors);
264
+ }
265
+ ```
266
+
267
+ You don't need a separate schema for field validation.
268
+
269
+ The same Zod schema is used for both operations.
270
+
271
+ ---
272
+
273
+ # 7. Nested Objects
274
+
275
+ `zod-validate` preserves the structure of nested validation errors.
276
+
277
+ Consider:
278
+
279
+ ```ts
280
+ const userSchema = z.object({
281
+ name: z.string("name_required"),
282
+
283
+ address: z.object({
284
+ city: z.string("city_required"),
285
+ street: z.string("street_required"),
286
+ }),
287
+ });
288
+ ```
289
+
290
+ Create the validator:
291
+
292
+ ```ts
293
+ const userValidator = createValidator(userSchema);
294
+ ```
295
+
296
+ Validate:
297
+
298
+ ```ts
299
+ const result = userValidator.validate({
300
+ name: "Bibek",
301
+ address: {
302
+ city: "",
303
+ street: "",
304
+ },
305
+ });
306
+ ```
307
+
308
+ The errors mirror the input structure:
309
+
310
+ ```ts
311
+ {
312
+ valid: false,
313
+ errors: {
314
+ address: {
315
+ city: "city_required",
316
+ street: "street_required"
317
+ }
318
+ }
319
+ }
320
+ ```
321
+
322
+ This is useful because the error structure follows the same hierarchy as the data.
323
+
324
+ ---
325
+
326
+ # 8. Nested Arrays
327
+
328
+ Arrays are also preserved.
329
+
330
+ For example:
331
+
332
+ ```ts
333
+ const buildingSchema = z.object({
334
+ name: z.string("building_name_required"),
335
+
336
+ floors: z.array(
337
+ z.object({
338
+ floor_number: z.number("floor_number_required"),
339
+
340
+ name: z.string("floor_name_required"),
341
+
342
+ units: z.array(
343
+ z.object({
344
+ unit_number: z.string("unit_number_required"),
345
+
346
+ rent: z.number("rent_required"),
347
+ }),
348
+ ),
349
+ }),
350
+ ),
351
+ });
352
+ ```
353
+
354
+ Create the validator:
355
+
356
+ ```ts
357
+ const buildingValidator = createValidator(buildingSchema);
358
+ ```
359
+
360
+ Suppose the second floor contains invalid data:
361
+
362
+ ```ts
363
+ const result = buildingValidator.validate({
364
+ name: "Sunrise Apartments",
365
+
366
+ floors: [
367
+ {
368
+ floor_number: 1,
369
+ name: "Ground Floor",
370
+ units: [
371
+ {
372
+ unit_number: "101",
373
+ rent: 15000,
374
+ },
375
+ ],
376
+ },
377
+
378
+ {
379
+ floor_number: 2,
380
+ name: "",
381
+ units: [
382
+ {
383
+ unit_number: "",
384
+ rent: -5000,
385
+ },
386
+ ],
387
+ },
388
+ ],
389
+ });
390
+ ```
391
+
392
+ The resulting errors follow the same structure:
393
+
394
+ ```ts
395
+ {
396
+ valid: false,
397
+ errors: {
398
+ floors: [
399
+ <empty>,
400
+ {
401
+ name: "floor_name_required",
402
+
403
+ units: [
404
+ {
405
+ unit_number: "unit_number_required",
406
+ rent: "rent_required"
407
+ }
408
+ ]
409
+ }
410
+ ]
411
+ }
412
+ }
413
+ ```
414
+
415
+ The error at:
416
+
417
+ ```ts
418
+ errors.floors[1].name;
419
+ ```
420
+
421
+ belongs to:
422
+
423
+ ```ts
424
+ floors[1].name;
425
+ ```
426
+
427
+ And:
428
+
429
+ ```ts
430
+ errors.floors[1].units[0].rent;
431
+ ```
432
+
433
+ belongs to:
434
+
435
+ ```ts
436
+ floors[1].units[0].rent;
437
+ ```
438
+
439
+ The structure is intentionally aligned with the original data.
440
+
441
+ ---
442
+
443
+ # 9. Using Nested Errors in React
444
+
445
+ This structure works naturally with component hierarchies.
446
+
447
+ For example:
448
+
449
+ ```tsx
450
+ <Building errors={errors} />
451
+ ```
452
+
453
+ The building component can pass the appropriate floor errors:
454
+
455
+ ```tsx
456
+ <Floors errors={errors.floors?.[floorIndex]} />
457
+ ```
458
+
459
+ The floor component can pass errors to a unit:
460
+
461
+ ```tsx
462
+ <Unit errors={errors?.units?.[unitIndex]} />
463
+ ```
464
+
465
+ And the unit component can access:
466
+
467
+ ```tsx
468
+ errors?.rent;
469
+ ```
470
+
471
+ For example:
472
+
473
+ ```tsx
474
+ <input />;
475
+
476
+ {
477
+ errors?.rent && <p>{errors.rent}</p>;
478
+ }
479
+ ```
480
+
481
+ This works because the validation error structure follows the data structure.
482
+
483
+ You don't need to convert:
484
+
485
+ ```text
486
+ floors[1].units[0].rent
487
+ ```
488
+
489
+ into a flat string just to find the error.
490
+
491
+ ---
492
+
493
+ # 10. Sparse Array Errors
494
+
495
+ When only some array items contain errors, the resulting error array can contain empty slots.
496
+
497
+ For example:
498
+
499
+ ```ts
500
+ [
501
+ <empty>,
502
+ {
503
+ name: "floor_name_required"
504
+ }
505
+ ]
506
+ ```
507
+
508
+ The empty slot represents an array item that has no validation error.
509
+
510
+ Therefore:
511
+
512
+ ```ts
513
+ errors.floors?.[0];
514
+ ```
515
+
516
+ returns:
517
+
518
+ ```ts
519
+ undefined;
520
+ ```
521
+
522
+ while:
523
+
524
+ ```ts
525
+ errors.floors?.[1];
526
+ ```
527
+
528
+ contains the second floor's errors.
529
+
530
+ This allows the error array to preserve the original data indexes.
531
+
532
+ ---
533
+
534
+ # 11. Validation Codes
535
+
536
+ A major part of the design is that the validation layer can return stable codes.
537
+
538
+ For example:
539
+
540
+ ```ts
541
+ const result = signup.validate(data);
542
+ ```
543
+
544
+ may produce:
545
+
546
+ ```ts
547
+ {
548
+ valid: false,
549
+ errors: {
550
+ mobile_no: "mobile_no_required",
551
+ password: "password_min_length"
552
+ }
553
+ }
554
+ ```
555
+
556
+ The validation layer doesn't need to know how these errors should be displayed.
557
+
558
+ This allows the application to decide what each code means.
559
+
560
+ For example:
561
+
562
+ ```ts
563
+ const messages = {
564
+ mobile_no_required: "Mobile number is required.",
565
+ password_min_length: "Password must be at least 8 characters.",
566
+ };
567
+ ```
568
+
569
+ ---
570
+
571
+ # 12. Resolve Validation Messages
572
+
573
+ Import:
574
+
575
+ ```ts
576
+ import { resolveValidationMessages } from "zod-validate";
577
+ ```
578
+
579
+ Then:
580
+
581
+ ```ts
582
+ const resolvedErrors = resolveValidationMessages(result.errors, messages);
583
+ ```
584
+
585
+ Given:
586
+
587
+ ```ts
588
+ const errors = {
589
+ mobile_no: "mobile_no_required",
590
+ password: "password_min_length",
591
+ };
592
+ ```
593
+
594
+ and:
595
+
596
+ ```ts
597
+ const messages = {
598
+ mobile_no_required: "Mobile number is required.",
599
+ password_min_length: "Password must be at least 8 characters.",
600
+ };
601
+ ```
602
+
603
+ the result is:
604
+
605
+ ```ts
606
+ {
607
+ mobile_no: "Mobile number is required.",
608
+ password: "Password must be at least 8 characters."
609
+ }
610
+ ```
611
+
612
+ ---
613
+
614
+ # 13. Missing Message Codes
615
+
616
+ A message does not have to exist for every validation code.
617
+
618
+ For example:
619
+
620
+ ```ts
621
+ const errors = {
622
+ mobile_no: "mobile_no_required",
623
+ password: "password_min_length",
624
+ };
625
+
626
+ const messages = {
627
+ mobile_no_required: "Mobile number is required.",
628
+ };
629
+ ```
630
+
631
+ The known message is resolved:
632
+
633
+ ```ts
634
+ {
635
+ mobile_no: "Mobile number is required.",
636
+ password: "password_min_length"
637
+ }
638
+ ```
639
+
640
+ An unknown code falls back to the original code.
641
+
642
+ This means missing translations do not silently disappear.
643
+
644
+ ---
645
+
646
+ # 14. Localized Messages
647
+
648
+ Messages can also be provided by locale.
649
+
650
+ ```ts
651
+ const messages = {
652
+ en: {
653
+ mobile_no_required: "Mobile number is required.",
654
+ password_min_length: "Password must be at least 8 characters.",
655
+ },
656
+
657
+ np: {
658
+ mobile_no_required: "मोबाइल नम्बर आवश्यक छ।",
659
+ password_min_length: "पासवर्ड कम्तीमा ८ अक्षरको हुनुपर्छ।",
660
+ },
661
+ };
662
+ ```
663
+
664
+ Resolve English messages:
665
+
666
+ ```ts
667
+ const errors = resolveValidationMessages(
668
+ validationResult.errors,
669
+ messages,
670
+ "en",
671
+ );
672
+ ```
673
+
674
+ Result:
675
+
676
+ ```ts
677
+ {
678
+ mobile_no: "Mobile number is required.",
679
+ password: "Password must be at least 8 characters."
680
+ }
681
+ ```
682
+
683
+ Resolve Nepali messages:
684
+
685
+ ```ts
686
+ const errors = resolveValidationMessages(
687
+ validationResult.errors,
688
+ messages,
689
+ "np",
690
+ );
691
+ ```
692
+
693
+ Result:
694
+
695
+ ```ts
696
+ {
697
+ mobile_no: "मोबाइल नम्बर आवश्यक छ।",
698
+ password: "पासवर्ड कम्तीमा ८ अक्षरको हुनुपर्छ।"
699
+ }
700
+ ```
701
+
702
+ ---
703
+
704
+ # 15. Default Locale
705
+
706
+ If no locale is supplied and the message configuration is localized, `en` is preferred when available.
707
+
708
+ For example:
709
+
710
+ ```ts
711
+ const messages = {
712
+ en: {
713
+ required: "This field is required.",
714
+ },
715
+
716
+ np: {
717
+ required: "यो field आवश्यक छ।",
718
+ },
719
+ };
720
+ ```
721
+
722
+ Then:
723
+
724
+ ```ts
725
+ resolveValidationMessages(errors, messages);
726
+ ```
727
+
728
+ uses the English messages.
729
+
730
+ If `en` does not exist, the first available locale is used.
731
+
732
+ ---
733
+
734
+ # 16. Plain Messages
735
+
736
+ Localization is optional.
737
+
738
+ You can simply use:
739
+
740
+ ```ts
741
+ const messages = {
742
+ required: "This field is required.",
743
+ invalid_email: "Invalid email address.",
744
+ };
745
+ ```
746
+
747
+ Then:
748
+
749
+ ```ts
750
+ resolveValidationMessages(errors, messages);
751
+ ```
752
+
753
+ No locale is required.
754
+
755
+ ---
756
+
757
+ # 17. Locale Configuration Validation
758
+
759
+ The resolver validates the message configuration.
760
+
761
+ A plain message map must contain string messages:
762
+
763
+ ```ts
764
+ const messages = {
765
+ required: "This field is required.",
766
+ invalid: "Invalid value.",
767
+ };
768
+ ```
769
+
770
+ A localized message map must contain message maps:
771
+
772
+ ```ts
773
+ const messages = {
774
+ en: {
775
+ required: "This field is required.",
776
+ },
777
+
778
+ np: {
779
+ required: "यो field आवश्यक छ।",
780
+ },
781
+ };
782
+ ```
783
+
784
+ Invalid message values are rejected instead of being silently accepted.
785
+
786
+ For example:
787
+
788
+ ```ts
789
+ const messages = {
790
+ en: {
791
+ required: 123,
792
+ },
793
+ };
794
+ ```
795
+
796
+ is invalid because validation messages must be strings.
797
+
798
+ ---
799
+
800
+ # 18. Frontend Use Case
801
+
802
+ A React form can use the library in two stages.
803
+
804
+ ### During field interaction
805
+
806
+ ```ts
807
+ const result = signup.validateField("mobile_no", mobileNo);
808
+
809
+ if (!result.valid) {
810
+ setErrors(result.errors);
811
+ }
812
+ ```
813
+
814
+ ### During submission
815
+
816
+ ```ts
817
+ const result = signup.validate(formData);
818
+
819
+ if (!result.valid) {
820
+ const errors = resolveValidationMessages(result.errors, messages);
821
+
822
+ setErrors(errors);
823
+ return;
824
+ }
825
+
826
+ submitForm(result.data);
827
+ ```
828
+
829
+ This gives the frontend:
830
+
831
+ ```text
832
+ Zod schema
833
+ ```
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ import type { ValidationResult } from "./types.js";
3
+ export declare const createValidator: <S extends z.ZodType>(schema: S) => {
4
+ validate(data: unknown): ValidationResult<z.infer<S>>;
5
+ validateField<K extends keyof z.infer<S>>(field: K, value: unknown): ValidationResult<z.infer<S>[K]>;
6
+ };
7
+ //# sourceMappingURL=createValidators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createValidators.d.ts","sourceRoot":"","sources":["../src/createValidators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGnD,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,CAAC,CAAC,OAAO,UAAU,CAAC;IAK1D,QAAQ,OAAO,OAAO,GAAG,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAIrD,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAC/B,CAAC,SACD,OAAO,GACb,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CASrC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { validateFieldEngine, validateObjectEngine } from "./engine.js";
3
+ export const createValidator = (schema) => {
4
+ const validateField = schema instanceof z.ZodObject ? validateFieldEngine(schema) : undefined;
5
+ return {
6
+ validate(data) {
7
+ return validateObjectEngine(schema, data);
8
+ },
9
+ validateField(field, value) {
10
+ if (!validateField)
11
+ throw new Error("validateField() can only be used with a Zod object schema.");
12
+ return validateField(field, value);
13
+ },
14
+ };
15
+ };
16
+ //# sourceMappingURL=createValidators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createValidators.js","sourceRoot":"","sources":["../src/createValidators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAsB,MAAS,EAAE,EAAE;IAChE,MAAM,aAAa,GACjB,MAAM,YAAY,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1E,OAAO;QACL,QAAQ,CAAC,IAAa;YACpB,OAAO,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC;QAED,aAAa,CACX,KAAQ,EACR,KAAc;YAEd,IAAI,CAAC,aAAa;gBAChB,MAAM,IAAI,KAAK,CACb,4DAA4D,CAC7D,CAAC;YAEJ,OAAO,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACrC,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ import type { ValidationResult } from "./types.js";
3
+ export declare const validateObjectEngine: <S extends z.ZodType>(schema: S, data: unknown) => ValidationResult<z.infer<S>>;
4
+ export declare const validateFieldEngine: <T extends z.ZodObject<any>>(schema: T) => <K extends keyof z.infer<T>>(field: K, value: unknown) => ValidationResult<z.infer<T>[K]>;
5
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,CAAC,CAAC,OAAO,UAC9C,CAAC,QACH,OAAO,KACZ,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CA0B7B,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,MAG/D,CAAC,SAAS,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SACzB,CAAC,SACD,OAAO,KACb,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAYlC,CAAC"}
package/dist/engine.js ADDED
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ export const validateObjectEngine = (schema, data) => {
3
+ const result = schema.safeParse(data);
4
+ if (result.success)
5
+ return { valid: true, data: result.data };
6
+ const errors = {};
7
+ for (const issue of result.error.issues) {
8
+ let current = errors;
9
+ const path = issue.path;
10
+ for (let i = 0; i < path.length; i++) {
11
+ const key = path[i];
12
+ const isLast = i === path.length - 1;
13
+ if (isLast) {
14
+ if (current[key] === undefined)
15
+ current[key] = issue.message;
16
+ }
17
+ else {
18
+ if (!current[key] || typeof current[key] !== "object")
19
+ current[key] = typeof path[i + 1] === "number" ? [] : {};
20
+ current = current[key];
21
+ }
22
+ }
23
+ }
24
+ return { valid: false, errors };
25
+ };
26
+ export const validateFieldEngine = (schema) => {
27
+ const shape = schema.shape;
28
+ return (field, value) => {
29
+ const result = shape[field].safeParse(value);
30
+ if (result.success)
31
+ return { valid: true, data: result.data };
32
+ const error = result.error.issues[0];
33
+ return {
34
+ valid: false,
35
+ errors: { [String(field)]: error?.message ?? "validation_error" },
36
+ };
37
+ };
38
+ };
39
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,MAAS,EACT,IAAa,EACiB,EAAE;IAChC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;IAE9D,MAAM,MAAM,GAAQ,EAAE,CAAC;IAEvB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACxC,IAAI,OAAO,GAAG,MAAM,CAAC;QACrB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QAExB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;YACrB,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;YAErC,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS;oBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC;YAC/D,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ;oBACnD,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAE3D,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAClC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAA6B,MAAS,EAAE,EAAE;IAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAE3B,OAAO,CACL,KAAQ,EACR,KAAc,EACmB,EAAE;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;QAE9D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAErC,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,IAAI,kBAAkB,EAAE;SAClE,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { createValidator } from "./createValidators.js";
2
+ export type { ValidationResult, ValidationErrors, ValidationMessages, } from "./types.js";
3
+ export { resolveValidationMessages } from "./messages.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createValidator } from "./createValidators.js";
2
+ export { resolveValidationMessages } from "./messages.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAMxD,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ValidationErrors, ValidationMessages } from "./types.js";
2
+ export declare const resolveValidationMessages: (errors: ValidationErrors, messages: ValidationMessages, locale?: string) => ValidationErrors;
3
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAEhB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAqHpB,eAAO,MAAM,yBAAyB,WAC5B,gBAAgB,YACd,kBAAkB,WACnB,MAAM,KACd,gBAGF,CAAC"}
@@ -0,0 +1,77 @@
1
+ const getMessageMap = (messages, locale) => {
2
+ const isPlain = isPlainMessageMap(messages);
3
+ const isLocalized = isLocaleMessageMap(messages);
4
+ if (!isPlain && !isLocalized) {
5
+ const localeError = getLocaleMessageMapError(messages);
6
+ if (localeError)
7
+ throw new Error(localeError);
8
+ throw new Error("Invalid validation messages. Messages must be either a plain message map or a locale-based message map.");
9
+ }
10
+ if (isPlain) {
11
+ if (locale)
12
+ throw new Error(`Locale "${locale}" was provided, but the validation messages are not localized.`);
13
+ return messages;
14
+ }
15
+ const locales = Object.keys(messages);
16
+ if (locales.length === 0)
17
+ throw new Error("No validation message locales have been defined.");
18
+ const selectedLocale = locale ?? (locales.includes("en") ? "en" : locales[0]);
19
+ if (!selectedLocale)
20
+ throw new Error("No validation message locale has been defined.");
21
+ const messageMap = messages[selectedLocale];
22
+ if (!messageMap)
23
+ throw new Error(`Validation messages for locale "${selectedLocale}" are not defined.`);
24
+ return messageMap;
25
+ };
26
+ const isPlainMessageMap = (value) => {
27
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
28
+ return false;
29
+ }
30
+ const values = Object.values(value);
31
+ return (values.length > 0 && values.every((message) => typeof message === "string"));
32
+ };
33
+ const isLocaleMessageMap = (value) => {
34
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
35
+ return false;
36
+ }
37
+ const localeMaps = Object.values(value);
38
+ return (localeMaps.length > 0 &&
39
+ localeMaps.every((messageMap) => isPlainMessageMap(messageMap)));
40
+ };
41
+ const getLocaleMessageMapError = (value) => {
42
+ if (typeof value !== "object" || value === null || Array.isArray(value))
43
+ return undefined;
44
+ for (const [locale, messageMap] of Object.entries(value)) {
45
+ if (typeof messageMap !== "object" ||
46
+ messageMap === null ||
47
+ Array.isArray(messageMap))
48
+ return `Validation messages for locale "${locale}" must be a message map.`;
49
+ for (const [code, message] of Object.entries(messageMap)) {
50
+ if (typeof message !== "string")
51
+ return `Validation message for locale "${locale}" and code "${code}" must be a string. Received ${typeof message}.`;
52
+ }
53
+ }
54
+ return undefined;
55
+ };
56
+ const resolveErrors = (errors, messageMap) => {
57
+ const resolved = {};
58
+ for (const key of Object.keys(errors)) {
59
+ const value = errors[key];
60
+ if (typeof value === "string") {
61
+ resolved[key] = messageMap[value] ?? value;
62
+ continue;
63
+ }
64
+ if (Array.isArray(value)) {
65
+ resolved[key] = value.map((item) => resolveErrors(item, messageMap));
66
+ continue;
67
+ }
68
+ if (value !== undefined)
69
+ resolved[key] = resolveErrors(value, messageMap);
70
+ }
71
+ return resolved;
72
+ };
73
+ export const resolveValidationMessages = (errors, messages, locale) => {
74
+ const messageMap = getMessageMap(messages, locale);
75
+ return resolveErrors(errors, messageMap);
76
+ };
77
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "zod-validate",
3
+ "version": "1.0.0",
4
+ "description": "Reusable validation utilities built on top of Zod for frontend and backend applications.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "test": "vitest",
14
+ "dev:test": "tsx test.ts"
15
+ },
16
+ "keywords": [
17
+ "zod",
18
+ "validation",
19
+ "typescript",
20
+ "frontend",
21
+ "backend"
22
+ ],
23
+ "author": "Bibek Bhattarai",
24
+ "license": "MIT",
25
+ "peerDependencies": {
26
+ "zod": "^4.6.2"
27
+ },
28
+ "devDependencies": {
29
+ "zod": "^4.6.2",
30
+ "tsx": "^4.23.13",
31
+ "typescript": "^7.0.2",
32
+ "vitest": "^5.0.0"
33
+ }
34
+ }