zod-validate 1.0.0 → 1.1.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 (2) hide show
  1. package/README.md +664 -238
  2. package/package.json +25 -12
package/README.md CHANGED
@@ -8,8 +8,9 @@ Reusable validation utilities built on top of [Zod](https://zod.dev/) for fronte
8
8
  - Individual field validation
9
9
  - Nested object validation
10
10
  - Nested array validation
11
- - Validation error codes
12
- - Human-readable message resolution
11
+ - Structured validation errors
12
+ - Optional validation error codes
13
+ - Optional human-readable message resolution
13
14
  - Localized validation messages
14
15
 
15
16
  The package does **not** replace Zod. Zod remains the validation engine.
@@ -19,39 +20,13 @@ The package does **not** replace Zod. Zod remains the validation engine.
19
20
  ## Installation
20
21
 
21
22
  ```bash
22
- npm install zod-validate zod
23
+ npm install zod-validate
23
24
  ```
24
25
 
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:
26
+ You continue to use Zod to define your schemas:
45
27
 
46
28
  ```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);
29
+ import { z } from "zod";
55
30
  ```
56
31
 
57
32
  ---
@@ -60,39 +35,31 @@ signup.validateField("mobile_no", value);
60
35
 
61
36
  ## 1. Define a Zod schema
62
37
 
63
- You continue to define your validation rules using Zod.
38
+ You define your validation rules using Zod as usual.
39
+
40
+ For example:
64
41
 
65
42
  ```ts
66
43
  import { z } from "zod";
67
44
 
68
45
  const signupSchema = z.object({
69
46
  mobile_no: z
70
- .string("mobile_no")
47
+ .string()
71
48
  .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"),
49
+ .min(1, "Mobile number is required.")
50
+ .regex(/^[0-9]+$/, "Mobile number must contain only digits.")
51
+ .regex(/^(98|97)/, "Mobile number must start with 98 or 97.")
52
+ .length(10, "Mobile number must be exactly 10 digits."),
76
53
 
77
- password: z.string("password").min(8, "password_min_length"),
54
+ password: z.string().min(8, "Password must be at least 8 characters."),
78
55
  });
79
56
  ```
80
57
 
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.
58
+ Zod messages are returned by `zod-validate` exactly as provided by the schema.
92
59
 
93
60
  ---
94
61
 
95
- # 2. Create a Validator
62
+ ## 2. Create a Validator
96
63
 
97
64
  Import `createValidator`:
98
65
 
@@ -106,7 +73,7 @@ Create a validator from your schema:
106
73
  const signup = createValidator(signupSchema);
107
74
  ```
108
75
 
109
- The validator is now tied to that schema.
76
+ The validator is now tied to that schema and can be reused wherever the schema needs to be validated.
110
77
 
111
78
  ---
112
79
 
@@ -137,9 +104,38 @@ The returned `data` is the data parsed by Zod.
137
104
 
138
105
  This means Zod transformations are also preserved.
139
106
 
107
+ For example:
108
+
109
+ ```ts
110
+ const schema = z.object({
111
+ name: z.string().trim(),
112
+ });
113
+ ```
114
+
115
+ If the input is:
116
+
117
+ ```ts
118
+ {
119
+ name: " John Doe ";
120
+ }
121
+ ```
122
+
123
+ the successful result contains the transformed value:
124
+
125
+ ```ts
126
+ {
127
+ valid: true,
128
+ data: {
129
+ name: "John Doe"
130
+ }
131
+ }
132
+ ```
133
+
140
134
  ---
141
135
 
142
- ## Invalid data
136
+ # 4. Invalid Data
137
+
138
+ Suppose the data is invalid:
143
139
 
144
140
  ```ts
145
141
  const result = signup.validate({
@@ -148,7 +144,105 @@ const result = signup.validate({
148
144
  });
149
145
  ```
150
146
 
151
- The result is:
147
+ Because the schema contains human-readable messages, the result is:
148
+
149
+ ```ts
150
+ {
151
+ valid: false,
152
+ errors: {
153
+ mobile_no: "Mobile number is required.",
154
+ password: "Password must be at least 8 characters."
155
+ }
156
+ }
157
+ ```
158
+
159
+ The errors remain associated with their original fields.
160
+
161
+ You can use them directly:
162
+
163
+ ```ts
164
+ if (!result.valid) {
165
+ console.log(result.errors.mobile_no);
166
+ }
167
+ ```
168
+
169
+ Output:
170
+
171
+ ```text
172
+ Mobile number is required.
173
+ ```
174
+
175
+ No additional message resolution is required.
176
+
177
+ ---
178
+
179
+ # Validation Messages
180
+
181
+ There are two ways to provide messages in your Zod schemas.
182
+
183
+ ## Direct Messages
184
+
185
+ The simplest approach is to put the human-readable message directly in the schema:
186
+
187
+ ```ts
188
+ const signupSchema = z.object({
189
+ mobile_no: z.string().min(1, "Mobile number is required."),
190
+ password: z.string().min(8, "Password must be at least 8 characters."),
191
+ });
192
+ ```
193
+
194
+ Then:
195
+
196
+ ```ts
197
+ const result = signup.validate({
198
+ mobile_no: "",
199
+ password: "123",
200
+ });
201
+ ```
202
+
203
+ returns:
204
+
205
+ ```ts
206
+ {
207
+ valid: false,
208
+ errors: {
209
+ mobile_no: "Mobile number is required.",
210
+ password: "Password must be at least 8 characters."
211
+ }
212
+ }
213
+ ```
214
+
215
+ You can use these messages directly:
216
+
217
+ ```ts
218
+ if (!result.valid) {
219
+ setErrors(result.errors);
220
+ }
221
+ ```
222
+
223
+ This approach is useful when your validation messages are simple and do not need to be managed separately.
224
+
225
+ ---
226
+
227
+ # Validation Codes
228
+
229
+ For larger applications, you may prefer to keep validation rules separate from human-readable messages.
230
+
231
+ Instead of putting the final message in the schema, use a stable validation code:
232
+
233
+ ```ts
234
+ const signupSchema = z.object({
235
+ mobile_no: z
236
+ .string()
237
+ .min(1, "mobile_no_required")
238
+ .regex(/^[0-9]+$/, "mobile_no_digits")
239
+ .length(10, "mobile_no_length"),
240
+
241
+ password: z.string().min(8, "password_min_length"),
242
+ });
243
+ ```
244
+
245
+ The validator then returns those codes:
152
246
 
153
247
  ```ts
154
248
  {
@@ -160,13 +254,257 @@ The result is:
160
254
  }
161
255
  ```
162
256
 
163
- The validator returns the validation codes rather than forcing a human-readable message into your validation layer.
257
+ The validation layer does not need to know how those codes should be displayed.
258
+
259
+ Your application can decide what each code means.
260
+
261
+ For example:
262
+
263
+ ```ts
264
+ const messages = {
265
+ mobile_no_required: "Mobile number is required.",
266
+ mobile_no_digits: "Mobile number must contain only digits.",
267
+ mobile_no_length: "Mobile number must be exactly 10 digits.",
268
+ password_min_length: "Password must be at least 8 characters.",
269
+ };
270
+ ```
271
+
272
+ This approach becomes particularly useful when messages need to be:
273
+
274
+ - Centralized
275
+ - Reused
276
+ - Changed independently from validation rules
277
+ - Localized
278
+ - Shared between different parts of an application
164
279
 
165
280
  ---
166
281
 
167
- # 4. Type-Safe Result Handling
282
+ # Resolving Validation Messages
283
+
284
+ `resolveValidationMessages()` is an optional utility for applications that use validation codes.
285
+
286
+ Import it:
287
+
288
+ ```ts
289
+ import { resolveValidationMessages } from "zod-validate";
290
+ ```
291
+
292
+ Given validation errors:
293
+
294
+ ```ts
295
+ const errors = {
296
+ mobile_no: "mobile_no_required",
297
+ password: "password_min_length",
298
+ };
299
+ ```
300
+
301
+ and a message map:
168
302
 
169
- The result is a discriminated union.
303
+ ```ts
304
+ const messages = {
305
+ mobile_no_required: "Mobile number is required.",
306
+ password_min_length: "Password must be at least 8 characters.",
307
+ };
308
+ ```
309
+
310
+ resolve the codes:
311
+
312
+ ```ts
313
+ const resolvedErrors = resolveValidationMessages(errors, messages);
314
+ ```
315
+
316
+ The result is:
317
+
318
+ ```ts
319
+ {
320
+ mobile_no: "Mobile number is required.",
321
+ password: "Password must be at least 8 characters."
322
+ }
323
+ ```
324
+
325
+ The structure of the errors is preserved.
326
+
327
+ For example:
328
+
329
+ ```ts
330
+ {
331
+ user: {
332
+ email: "email_invalid";
333
+ }
334
+ }
335
+ ```
336
+
337
+ can become:
338
+
339
+ ```ts
340
+ {
341
+ user: {
342
+ email: "Invalid email address.";
343
+ }
344
+ }
345
+ ```
346
+
347
+ You do **not** need to use `resolveValidationMessages()` when your Zod schema already contains human-readable messages.
348
+
349
+ It is an optional layer for applications that choose to use validation codes.
350
+
351
+ ---
352
+
353
+ # Missing Message Codes
354
+
355
+ A message does not have to exist for every validation code.
356
+
357
+ For example:
358
+
359
+ ```ts
360
+ const errors = {
361
+ mobile_no: "mobile_no_required",
362
+ password: "password_min_length",
363
+ };
364
+
365
+ const messages = {
366
+ mobile_no_required: "Mobile number is required.",
367
+ };
368
+ ```
369
+
370
+ Resolving the messages produces:
371
+
372
+ ```ts
373
+ {
374
+ mobile_no: "Mobile number is required.",
375
+ password: "password_min_length"
376
+ }
377
+ ```
378
+
379
+ Known codes are resolved.
380
+
381
+ Unknown codes fall back to the original code.
382
+
383
+ This means missing messages do not silently disappear.
384
+
385
+ ---
386
+
387
+ # Localized Validation Messages
388
+
389
+ Validation codes can also be resolved using localized message maps.
390
+
391
+ For example:
392
+
393
+ ```ts
394
+ const messages = {
395
+ en: {
396
+ mobile_no_required: "Mobile number is required.",
397
+ password_min_length: "Password must be at least 8 characters.",
398
+ },
399
+
400
+ np: {
401
+ mobile_no_required: "मोबाइल नम्बर आवश्यक छ।",
402
+ password_min_length: "पासवर्ड कम्तीमा ८ अक्षरको हुनुपर्छ।",
403
+ },
404
+ };
405
+ ```
406
+
407
+ Resolve English messages:
408
+
409
+ ```ts
410
+ const errors = resolveValidationMessages(
411
+ validationResult.errors,
412
+ messages,
413
+ "en",
414
+ );
415
+ ```
416
+
417
+ Result:
418
+
419
+ ```ts
420
+ {
421
+ mobile_no: "Mobile number is required.",
422
+ password: "Password must be at least 8 characters."
423
+ }
424
+ ```
425
+
426
+ Resolve Nepali messages:
427
+
428
+ ```ts
429
+ const errors = resolveValidationMessages(
430
+ validationResult.errors,
431
+ messages,
432
+ "np",
433
+ );
434
+ ```
435
+
436
+ Result:
437
+
438
+ ```ts
439
+ {
440
+ mobile_no: "मोबाइल नम्बर आवश्यक छ।",
441
+ password: "पासवर्ड कम्तीमा ८ अक्षरको हुनुपर्छ।"
442
+ }
443
+ ```
444
+
445
+ ---
446
+
447
+ # Default Locale
448
+
449
+ If no locale is supplied and the message configuration is localized, `en` is preferred when available.
450
+
451
+ For example:
452
+
453
+ ```ts
454
+ const messages = {
455
+ en: {
456
+ required: "This field is required.",
457
+ },
458
+
459
+ np: {
460
+ required: "यो field आवश्यक छ।",
461
+ },
462
+ };
463
+ ```
464
+
465
+ Then:
466
+
467
+ ```ts
468
+ resolveValidationMessages(errors, messages);
469
+ ```
470
+
471
+ uses the English messages.
472
+
473
+ If `en` does not exist, the first available locale is used.
474
+ You can always explicitly choose a locale:
475
+
476
+ ```ts
477
+ resolveValidationMessages(errors, messages, "np");
478
+ ```
479
+
480
+ ---
481
+
482
+ # Plain Message Maps
483
+
484
+ Localization is optional.
485
+
486
+ You can provide a normal message map:
487
+
488
+ ```ts
489
+ const messages = {
490
+ required: "This field is required.",
491
+ invalid_email: "Invalid email address.",
492
+ };
493
+ ```
494
+
495
+ Then:
496
+
497
+ ```ts
498
+ resolveValidationMessages(errors, messages);
499
+ ```
500
+
501
+ No locale is required.
502
+
503
+ ---
504
+
505
+ # 5. Type-Safe Result Handling
506
+
507
+ The result returned by `createValidator()` is a discriminated union.
170
508
 
171
509
  ```ts
172
510
  const result = signup.validate(data);
@@ -196,7 +534,7 @@ means `result.errors` is available.
196
534
 
197
535
  ---
198
536
 
199
- # 5. Validate a Single Field
537
+ # 6. Validate a Single Field
200
538
 
201
539
  For frontend forms, you often don't want to validate the entire form.
202
540
 
@@ -227,22 +565,27 @@ You get:
227
565
  {
228
566
  valid: false,
229
567
  errors: {
230
- mobile_no: "mobile_no_digits"
568
+ mobile_no: "Mobile number must contain only digits."
231
569
  }
232
570
  }
233
571
  ```
234
572
 
235
- This makes the same schema useful for both:
573
+ If your schema uses validation codes instead:
236
574
 
237
- ```text
238
- Whole form validation
239
- +
240
- Individual field validation
575
+ ```ts
576
+ {
577
+ valid: false,
578
+ errors: {
579
+ mobile_no: "mobile_no_digits"
580
+ }
581
+ }
241
582
  ```
242
583
 
584
+ The same schema can therefore be used for both whole-form and individual-field validation.
585
+
243
586
  ---
244
587
 
245
- # 6. Why Field Validation Is Useful
588
+ # 7. Why Field Validation Is Useful
246
589
 
247
590
  A frontend form might use the validator like this:
248
591
 
@@ -270,7 +613,7 @@ The same Zod schema is used for both operations.
270
613
 
271
614
  ---
272
615
 
273
- # 7. Nested Objects
616
+ # 8. Nested Objects
274
617
 
275
618
  `zod-validate` preserves the structure of nested validation errors.
276
619
 
@@ -297,7 +640,8 @@ Validate:
297
640
 
298
641
  ```ts
299
642
  const result = userValidator.validate({
300
- name: "Bibek",
643
+ name: "John Doe",
644
+
301
645
  address: {
302
646
  city: "",
303
647
  street: "",
@@ -305,15 +649,42 @@ const result = userValidator.validate({
305
649
  });
306
650
  ```
307
651
 
308
- The errors mirror the input structure:
652
+ The errors mirror the input structure:
653
+
654
+ ```ts
655
+ {
656
+ valid: false,
657
+ errors: {
658
+ address: {
659
+ city: "city_required",
660
+ street: "street_required"
661
+ }
662
+ }
663
+ }
664
+ ```
665
+
666
+ The same structure is preserved when using direct messages:
667
+
668
+ ```ts
669
+ const userSchema = z.object({
670
+ name: z.string("Name is required."),
671
+
672
+ address: z.object({
673
+ city: z.string("City is required."),
674
+ street: z.string("Street is required."),
675
+ }),
676
+ });
677
+ ```
678
+
679
+ The resulting errors are:
309
680
 
310
681
  ```ts
311
682
  {
312
683
  valid: false,
313
684
  errors: {
314
685
  address: {
315
- city: "city_required",
316
- street: "street_required"
686
+ city: "City is required.",
687
+ street: "Street is required."
317
688
  }
318
689
  }
319
690
  }
@@ -323,7 +694,7 @@ This is useful because the error structure follows the same hierarchy as the dat
323
694
 
324
695
  ---
325
696
 
326
- # 8. Nested Arrays
697
+ # 9. Nested Arrays
327
698
 
328
699
  Arrays are also preserved.
329
700
 
@@ -342,7 +713,6 @@ const buildingSchema = z.object({
342
713
  units: z.array(
343
714
  z.object({
344
715
  unit_number: z.string("unit_number_required"),
345
-
346
716
  rent: z.number("rent_required"),
347
717
  }),
348
718
  ),
@@ -367,6 +737,7 @@ const result = buildingValidator.validate({
367
737
  {
368
738
  floor_number: 1,
369
739
  name: "Ground Floor",
740
+
370
741
  units: [
371
742
  {
372
743
  unit_number: "101",
@@ -378,6 +749,7 @@ const result = buildingValidator.validate({
378
749
  {
379
750
  floor_number: 2,
380
751
  name: "",
752
+
381
753
  units: [
382
754
  {
383
755
  unit_number: "",
@@ -440,7 +812,7 @@ The structure is intentionally aligned with the original data.
440
812
 
441
813
  ---
442
814
 
443
- # 9. Using Nested Errors in React
815
+ # 10. Using Nested Errors in React
444
816
 
445
817
  This structure works naturally with component hierarchies.
446
818
 
@@ -471,8 +843,6 @@ errors?.rent;
471
843
  For example:
472
844
 
473
845
  ```tsx
474
- <input />;
475
-
476
846
  {
477
847
  errors?.rent && <p>{errors.rent}</p>;
478
848
  }
@@ -490,7 +860,7 @@ into a flat string just to find the error.
490
860
 
491
861
  ---
492
862
 
493
- # 10. Sparse Array Errors
863
+ # 11. Sparse Array Errors
494
864
 
495
865
  When only some array items contain errors, the resulting error array can contain empty slots.
496
866
 
@@ -499,6 +869,7 @@ For example:
499
869
  ```ts
500
870
  [
501
871
  <empty>,
872
+
502
873
  {
503
874
  name: "floor_name_required"
504
875
  }
@@ -531,303 +902,358 @@ This allows the error array to preserve the original data indexes.
531
902
 
532
903
  ---
533
904
 
534
- # 11. Validation Codes
905
+ # 12. Choosing Between Messages and Codes
535
906
 
536
- A major part of the design is that the validation layer can return stable codes.
907
+ Both approaches are supported.
537
908
 
538
- For example:
909
+ ### Use direct messages when:
910
+
911
+ - Your messages are simple.
912
+ - You don't need localization.
913
+ - You want the schema to contain the final message.
914
+ - You want to use `result.errors` directly.
915
+
916
+ Example:
539
917
 
540
918
  ```ts
541
- const result = signup.validate(data);
919
+ z.string().min(1, "Name is required.");
542
920
  ```
543
921
 
544
- may produce:
922
+ Result:
545
923
 
546
924
  ```ts
547
925
  {
548
- valid: false,
549
- errors: {
550
- mobile_no: "mobile_no_required",
551
- password: "password_min_length"
552
- }
926
+ name: "Name is required.";
553
927
  }
554
928
  ```
555
929
 
556
- The validation layer doesn't need to know how these errors should be displayed.
930
+ ### Use validation codes when:
557
931
 
558
- This allows the application to decide what each code means.
932
+ - Messages need to be centralized.
933
+ - Multiple parts of the application share the same messages.
934
+ - Messages need to be localized.
935
+ - You want to change wording without changing validation rules.
936
+ - The validation layer should return stable identifiers rather than UI text.
559
937
 
560
- For example:
938
+ Example:
561
939
 
562
940
  ```ts
563
- const messages = {
564
- mobile_no_required: "Mobile number is required.",
565
- password_min_length: "Password must be at least 8 characters.",
566
- };
941
+ z.string().min(1, "name_required");
567
942
  ```
568
943
 
569
- ---
570
-
571
- # 12. Resolve Validation Messages
572
-
573
- Import:
944
+ Result:
574
945
 
575
946
  ```ts
576
- import { resolveValidationMessages } from "zod-validate";
947
+ {
948
+ name: "name_required";
949
+ }
577
950
  ```
578
951
 
579
- Then:
952
+ Then resolve it:
580
953
 
581
954
  ```ts
582
- const resolvedErrors = resolveValidationMessages(result.errors, messages);
955
+ resolveValidationMessages(errors, messages);
583
956
  ```
584
957
 
585
- Given:
958
+ There is no requirement to use validation codes.
959
+
960
+ There is also no requirement to use `resolveValidationMessages()`.
961
+
962
+ ---
963
+
964
+ # 13. Locale Configuration Validation
965
+
966
+ `resolveValidationMessages()` validates the message configuration it receives.
967
+
968
+ A plain message map must contain string messages:
586
969
 
587
970
  ```ts
588
- const errors = {
589
- mobile_no: "mobile_no_required",
590
- password: "password_min_length",
971
+ const messages = {
972
+ required: "This field is required.",
973
+ invalid: "Invalid value.",
591
974
  };
592
975
  ```
593
976
 
594
- and:
977
+ A localized message map must contain message maps:
595
978
 
596
979
  ```ts
597
980
  const messages = {
598
- mobile_no_required: "Mobile number is required.",
599
- password_min_length: "Password must be at least 8 characters.",
981
+ en: {
982
+ required: "This field is required.",
983
+ },
984
+
985
+ np: {
986
+ required: "यो field आवश्यक छ।",
987
+ },
600
988
  };
601
989
  ```
602
990
 
603
- the result is:
991
+ Invalid message values are rejected instead of being silently accepted.
992
+
993
+ For example:
604
994
 
605
995
  ```ts
606
- {
607
- mobile_no: "Mobile number is required.",
608
- password: "Password must be at least 8 characters."
609
- }
996
+ const messages = {
997
+ en: {
998
+ required: 123,
999
+ },
1000
+ };
610
1001
  ```
611
1002
 
1003
+ is invalid because validation messages must be strings.
1004
+
612
1005
  ---
613
1006
 
614
- # 13. Missing Message Codes
1007
+ # 14. Frontend Use Case
615
1008
 
616
- A message does not have to exist for every validation code.
1009
+ A React form can use the library without requiring message resolution.
617
1010
 
618
- For example:
1011
+ ## During field interaction
1012
+
1013
+ If your schema uses direct messages:
619
1014
 
620
1015
  ```ts
621
- const errors = {
622
- mobile_no: "mobile_no_required",
623
- password: "password_min_length",
624
- };
1016
+ const result = signup.validateField("mobile_no", mobileNo);
625
1017
 
626
- const messages = {
627
- mobile_no_required: "Mobile number is required.",
628
- };
1018
+ if (!result.valid) {
1019
+ setMobileError(result.errors.mobile_no);
1020
+ }
629
1021
  ```
630
1022
 
631
- The known message is resolved:
1023
+ The error can be displayed immediately.
1024
+
1025
+ If your schema uses validation codes:
632
1026
 
633
1027
  ```ts
634
- {
635
- mobile_no: "Mobile number is required.",
636
- password: "password_min_length"
1028
+ const result = signup.validateField("mobile_no", mobileNo);
1029
+
1030
+ if (!result.valid) {
1031
+ setMobileError(result.errors.mobile_no);
637
1032
  }
638
1033
  ```
639
1034
 
640
- An unknown code falls back to the original code.
1035
+ the error will contain the code instead.
641
1036
 
642
- This means missing translations do not silently disappear.
1037
+ You can resolve it when appropriate:
643
1038
 
644
- ---
1039
+ ```ts
1040
+ const errors = resolveValidationMessages(result.errors, messages);
1041
+ ```
645
1042
 
646
- # 14. Localized Messages
1043
+ ## During submission
647
1044
 
648
- Messages can also be provided by locale.
1045
+ With direct messages:
649
1046
 
650
1047
  ```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
- },
1048
+ const result = signup.validate(formData);
656
1049
 
657
- np: {
658
- mobile_no_required: "मोबाइल नम्बर आवश्यक छ।",
659
- password_min_length: "पासवर्ड कम्तीमा ८ अक्षरको हुनुपर्छ।",
660
- },
661
- };
1050
+ if (!result.valid) {
1051
+ setErrors(result.errors);
1052
+ return;
1053
+ }
1054
+
1055
+ submitForm(result.data);
662
1056
  ```
663
1057
 
664
- Resolve English messages:
1058
+ With validation codes:
665
1059
 
666
1060
  ```ts
667
- const errors = resolveValidationMessages(
668
- validationResult.errors,
669
- messages,
670
- "en",
671
- );
672
- ```
1061
+ const result = signup.validate(formData);
673
1062
 
674
- Result:
1063
+ if (!result.valid) {
1064
+ const errors = resolveValidationMessages(result.errors, messages);
675
1065
 
676
- ```ts
677
- {
678
- mobile_no: "Mobile number is required.",
679
- password: "Password must be at least 8 characters."
1066
+ setErrors(errors);
1067
+ return;
680
1068
  }
1069
+
1070
+ submitForm(result.data);
681
1071
  ```
682
1072
 
683
- Resolve Nepali messages:
1073
+ This keeps the two approaches independent while allowing them to use the same validator.
1074
+
1075
+ ---
1076
+
1077
+ # 15. Backend Use Case
1078
+
1079
+ The same validator can be used on the backend.
1080
+
1081
+ For example:
684
1082
 
685
1083
  ```ts
686
- const errors = resolveValidationMessages(
687
- validationResult.errors,
688
- messages,
689
- "np",
690
- );
1084
+ const result = signup.validate(req.body);
1085
+
1086
+ if (!result.valid) {
1087
+ return res.status(400).json({
1088
+ errors: result.errors,
1089
+ });
1090
+ }
691
1091
  ```
692
1092
 
693
- Result:
1093
+ When using validation codes, the backend can return stable codes:
694
1094
 
695
- ```ts
1095
+ ```json
696
1096
  {
697
- mobile_no: "मोबाइल नम्बर आवश्यक छ।",
698
- password: "पासवर्ड कम्तीमा ८ अक्षरको हुनुपर्छ।"
1097
+ "errors": {
1098
+ "mobile_no": "mobile_no_required",
1099
+ "password": "password_min_length"
1100
+ }
699
1101
  }
700
1102
  ```
701
1103
 
1104
+ The frontend can then decide how those codes should be displayed.
1105
+
1106
+ This allows the backend validation rules and frontend presentation to remain separate.
1107
+
702
1108
  ---
703
1109
 
704
- # 15. Default Locale
1110
+ # 16. Zod Remains the Validation Engine
705
1111
 
706
- If no locale is supplied and the message configuration is localized, `en` is preferred when available.
1112
+ `zod-validate` does not replace Zod.
707
1113
 
708
- For example:
1114
+ Your schemas are still Zod schemas:
709
1115
 
710
1116
  ```ts
711
- const messages = {
712
- en: {
713
- required: "This field is required.",
714
- },
715
-
716
- np: {
717
- required: "यो field आवश्यक छ।",
718
- },
719
- };
1117
+ const schema = z.object({
1118
+ name: z.string().min(1, "Name is required."),
1119
+ });
720
1120
  ```
721
1121
 
722
- Then:
1122
+ Zod performs the actual validation.
723
1123
 
724
- ```ts
725
- resolveValidationMessages(errors, messages);
726
- ```
1124
+ `zod-validate` provides a reusable layer around the result:
727
1125
 
728
- uses the English messages.
1126
+ ```text
1127
+ Zod schema
1128
+
1129
+ Zod validation
1130
+
1131
+ createValidator()
1132
+
1133
+ Structured validation result
1134
+ ```
729
1135
 
730
- If `en` does not exist, the first available locale is used.
1136
+ The package does not introduce another validation language or replace Zod's schema API.
731
1137
 
732
1138
  ---
733
1139
 
734
- # 16. Plain Messages
1140
+ # API Summary
735
1141
 
736
- Localization is optional.
1142
+ ## `createValidator(schema)`
737
1143
 
738
- You can simply use:
1144
+ Creates a reusable validator from a Zod schema.
739
1145
 
740
1146
  ```ts
741
- const messages = {
742
- required: "This field is required.",
743
- invalid_email: "Invalid email address.",
744
- };
1147
+ const validator = createValidator(schema);
745
1148
  ```
746
1149
 
747
- Then:
1150
+ ### `validator.validate(data)`
1151
+
1152
+ Validates the complete value.
748
1153
 
749
1154
  ```ts
750
- resolveValidationMessages(errors, messages);
1155
+ const result = validator.validate(data);
751
1156
  ```
752
1157
 
753
- No locale is required.
1158
+ Returns either:
754
1159
 
755
- ---
1160
+ ```ts
1161
+ {
1162
+ valid: true,
1163
+ data: parsedData
1164
+ }
1165
+ ```
1166
+
1167
+ or:
756
1168
 
757
- # 17. Locale Configuration Validation
1169
+ ```ts
1170
+ {
1171
+ valid: false,
1172
+ errors: validationErrors
1173
+ }
1174
+ ```
758
1175
 
759
- The resolver validates the message configuration.
1176
+ ### `validator.validateField(field, value)`
760
1177
 
761
- A plain message map must contain string messages:
1178
+ Validates an individual field of a Zod object schema.
762
1179
 
763
1180
  ```ts
764
- const messages = {
765
- required: "This field is required.",
766
- invalid: "Invalid value.",
767
- };
1181
+ const result = validator.validateField("mobile_no", value);
768
1182
  ```
769
1183
 
770
- A localized message map must contain message maps:
1184
+ Returns either:
771
1185
 
772
1186
  ```ts
773
- const messages = {
774
- en: {
775
- required: "This field is required.",
776
- },
1187
+ {
1188
+ valid: true,
1189
+ data: parsedValue
1190
+ }
1191
+ ```
777
1192
 
778
- np: {
779
- required: "यो field आवश्यक छ।",
780
- },
781
- };
1193
+ or:
1194
+
1195
+ ```ts
1196
+ {
1197
+ valid: false,
1198
+ errors: {
1199
+ mobile_no: "validation message or code"
1200
+ }
1201
+ }
782
1202
  ```
783
1203
 
784
- Invalid message values are rejected instead of being silently accepted.
1204
+ ---
785
1205
 
786
- For example:
1206
+ ## `resolveValidationMessages(errors, messages, locale?)`
1207
+
1208
+ Optionally resolves validation codes into human-readable messages.
787
1209
 
788
1210
  ```ts
789
- const messages = {
790
- en: {
791
- required: 123,
792
- },
793
- };
1211
+ const resolvedErrors = resolveValidationMessages(errors, messages);
794
1212
  ```
795
1213
 
796
- is invalid because validation messages must be strings.
1214
+ For localized messages:
1215
+
1216
+ ```ts
1217
+ const resolvedErrors = resolveValidationMessages(errors, messages, "np");
1218
+ ```
1219
+
1220
+ This function is only needed when you choose to manage validation messages separately from your Zod schemas.
797
1221
 
798
1222
  ---
799
1223
 
800
- # 18. Frontend Use Case
1224
+ # Design Philosophy
801
1225
 
802
- A React form can use the library in two stages.
1226
+ `zod-validate` intentionally stays small.
803
1227
 
804
- ### During field interaction
1228
+ It does not try to replace Zod or create a new validation system.
805
1229
 
806
- ```ts
807
- const result = signup.validateField("mobile_no", mobileNo);
1230
+ Instead, it focuses on a few useful problems around Zod validation:
808
1231
 
809
- if (!result.valid) {
810
- setErrors(result.errors);
811
- }
812
- ```
1232
+ - Reusable validators
1233
+ - Whole-object validation
1234
+ - Field-level validation
1235
+ - Structured nested errors
1236
+ - Preserving array indexes
1237
+ - Optional validation codes
1238
+ - Optional message resolution
1239
+ - Optional localization
813
1240
 
814
- ### During submission
1241
+ You can keep your schemas simple with direct messages:
815
1242
 
816
1243
  ```ts
817
- const result = signup.validate(formData);
818
-
819
- if (!result.valid) {
820
- const errors = resolveValidationMessages(result.errors, messages);
1244
+ z.string().min(1, "Name is required.");
1245
+ ```
821
1246
 
822
- setErrors(errors);
823
- return;
824
- }
1247
+ or use stable validation codes when your application benefits from separating validation rules from presentation:
825
1248
 
826
- submitForm(result.data);
1249
+ ```ts
1250
+ z.string().min(1, "name_required");
827
1251
  ```
828
1252
 
829
- This gives the frontend:
1253
+ Both approaches work with the same validator.
830
1254
 
831
- ```text
832
- Zod schema
833
- ```
1255
+ ---
1256
+
1257
+ # License
1258
+
1259
+ MIT
package/package.json CHANGED
@@ -1,32 +1,45 @@
1
1
  {
2
2
  "name": "zod-validate",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Reusable validation utilities built on top of Zod for frontend and backend applications.",
5
+ "keywords": [
6
+ "zod",
7
+ "validation",
8
+ "typescript",
9
+ "frontend",
10
+ "backend"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Bibek Bhattarai",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/dmaya0393/zod-validate.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/dmaya0393/zod-validate/issues"
20
+ },
5
21
  "type": "module",
6
22
  "main": "./dist/index.js",
7
23
  "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ }
29
+ },
8
30
  "files": [
9
31
  "dist"
10
32
  ],
11
33
  "scripts": {
12
34
  "build": "tsc",
13
35
  "test": "vitest",
36
+ "test:run": "vitest run",
14
37
  "dev:test": "tsx test.ts"
15
38
  },
16
- "keywords": [
17
- "zod",
18
- "validation",
19
- "typescript",
20
- "frontend",
21
- "backend"
22
- ],
23
- "author": "Bibek Bhattarai",
24
- "license": "MIT",
25
- "peerDependencies": {
39
+ "dependencies": {
26
40
  "zod": "^4.6.2"
27
41
  },
28
42
  "devDependencies": {
29
- "zod": "^4.6.2",
30
43
  "tsx": "^4.23.13",
31
44
  "typescript": "^7.0.2",
32
45
  "vitest": "^5.0.0"