@zod-utils/core 3.0.0 → 5.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/README.md CHANGED
@@ -22,20 +22,137 @@ npm install @zod-utils/core zod
22
22
 
23
23
  ## Features
24
24
 
25
- - 🎯 **Extract defaults** - Get default values from Zod schemas
26
- - **Check validation requirements** - Determine if fields will error on empty input
27
- - 🔍 **Extract validation checks** - Get all validation constraints (min/max, formats, patterns, etc.)
28
- - 🔧 **Schema utilities** - Unwrap and manipulate schema types
29
- - 📦 **Zero dependencies** - Only requires Zod as a peer dependency
30
- - 🌐 **Universal** - Works in Node.js, browsers, and any TypeScript project
25
+ - **Extract defaults** - Get default values from Zod schemas
26
+ - **Check validation requirements** - Determine if fields will error on empty input
27
+ - **Extract validation checks** - Get all validation constraints (min/max, formats, patterns, etc.)
28
+ - **Schema utilities** - Unwrap and manipulate schema types
29
+ - **Zero dependencies** - Only requires Zod as a peer dependency
30
+ - **Universal** - Works in Node.js, browsers, and any TypeScript project
31
+
32
+ ---
33
+
34
+ - [@zod-utils/core](#zod-utilscore)
35
+ - [Installation](#installation)
36
+ - [Related Packages](#related-packages)
37
+ - [Features](#features)
38
+ - [Quick Start](#quick-start)
39
+ - [API Reference](#api-reference)
40
+ - [`getSchemaDefaults(schema, options?)`](#getschemadefaultsschema-options)
41
+ - [Basic Example](#basic-example)
42
+ - [With Optional and Nullable Fields](#with-optional-and-nullable-fields)
43
+ - [With Function Defaults](#with-function-defaults)
44
+ - [Nested Objects - Important Behavior](#nested-objects---important-behavior)
45
+ - [With Discriminated Unions](#with-discriminated-unions)
46
+ - [With Schema Transforms](#with-schema-transforms)
47
+ - [`requiresValidInput(field)`](#requiresvalidinputfield)
48
+ - [How It Works](#how-it-works)
49
+ - [String Fields](#string-fields)
50
+ - [Number Fields](#number-fields)
51
+ - [Boolean Fields](#boolean-fields)
52
+ - [Array Fields](#array-fields)
53
+ - [Object Fields](#object-fields)
54
+ - [Other Types](#other-types)
55
+ - [Real-World Form Example](#real-world-form-example)
56
+ - [`extractDefaultValue(field)`](#extractdefaultvaluefield)
57
+ - [Basic Usage](#basic-usage)
58
+ - [With Wrappers](#with-wrappers)
59
+ - [With Function Defaults](#with-function-defaults-1)
60
+ - [With Transforms](#with-transforms)
61
+ - [With Unions](#with-unions)
62
+ - [`getPrimitiveType(field)`](#getprimitivetypefield)
63
+ - [Basic Usage](#basic-usage-1)
64
+ - [Stops at Arrays](#stops-at-arrays)
65
+ - [With Transforms](#with-transforms-1)
66
+ - [With Unions](#with-unions-1)
67
+ - [`removeDefault(field)`](#removedefaultfield)
68
+ - [`getFieldChecks(field)`](#getfieldchecksfield)
69
+ - [String Validations](#string-validations)
70
+ - [Number Validations](#number-validations)
71
+ - [Array Validations](#array-validations)
72
+ - [Date Validations](#date-validations)
73
+ - [Unwrapping Behavior](#unwrapping-behavior)
74
+ - [No Constraints](#no-constraints)
75
+ - [Using Checks in UI](#using-checks-in-ui)
76
+ - [`extractDiscriminatedSchema(props)`](#extractdiscriminatedschemaprops)
77
+ - [With Different Discriminator Types](#with-different-discriminator-types)
78
+ - [`extractFieldFromSchema(props)`](#extractfieldfromschemaprops)
79
+ - [Basic Usage](#basic-usage-2)
80
+ - [Nested Paths](#nested-paths)
81
+ - [Array Element Access](#array-element-access)
82
+ - [With Discriminated Unions](#with-discriminated-unions-1)
83
+ - [With Transforms](#with-transforms-2)
84
+ - [`extendWithMeta(field, transform)`](#extendwithmetafield-transform)
85
+ - [Use Case: Shared Field Definitions with i18n](#use-case-shared-field-definitions-with-i18n)
86
+ - [`toFieldSelector(props)`](#tofieldselectorprops)
87
+ - [Type Utilities](#type-utilities)
88
+ - [`Simplify<T>`](#simplifyt)
89
+ - [`Paths<T, FilterType?, Strict?>`](#pathst-filtertype-strict)
90
+ - [Basic Usage](#basic-usage-3)
91
+ - [Filter by Type](#filter-by-type)
92
+ - [Strict vs Non-Strict Mode](#strict-vs-non-strict-mode)
93
+ - [`ValidPaths<TSchema, TDiscriminatorKey?, TDiscriminatorValue?, TFilterType?, TStrict?>`](#validpathstschema-tdiscriminatorkey-tdiscriminatorvalue-tfiltertype-tstrict)
94
+ - [Basic Usage](#basic-usage-4)
95
+ - [Filter by Type](#filter-by-type-1)
96
+ - [With Discriminated Unions](#with-discriminated-unions-2)
97
+ - [`FieldSelector<TSchema, TName, TDiscriminatorKey?, TDiscriminatorValue?, TFilterType?, TStrict?>`](#fieldselectortschema-tname-tdiscriminatorkey-tdiscriminatorvalue-tfiltertype-tstrict)
98
+ - [`DiscriminatedInput<TSchema, TDiscriminatorKey, TDiscriminatorValue>`](#discriminatedinputtschema-tdiscriminatorkey-tdiscriminatorvalue)
99
+ - [Migration Guide](#migration-guide)
100
+ - [Migrating to v3.0.0](#migrating-to-v300)
101
+ - [`ValidPathsOfType` removed → Use `ValidPaths` with type filtering](#validpathsoftype-removed--use-validpaths-with-type-filtering)
102
+ - [Migrating to v4.0.0](#migrating-to-v400)
103
+ - [`mergeFieldSelectorProps` renamed → Use `toFieldSelector`](#mergefieldselectorprops-renamed--use-tofieldselector)
104
+ - [License](#license)
105
+
106
+ ---
107
+
108
+ ## Quick Start
109
+
110
+ ```typescript
111
+ import { z } from "zod";
112
+ import {
113
+ getSchemaDefaults,
114
+ requiresValidInput,
115
+ extractDefaultValue,
116
+ getPrimitiveType,
117
+ getFieldChecks,
118
+ } from "@zod-utils/core";
119
+
120
+ // Define your schema
121
+ const userSchema = z.object({
122
+ name: z.string().min(1),
123
+ email: z.string().email(),
124
+ age: z.number().min(18).max(120).default(25),
125
+ bio: z.string().optional(),
126
+ tags: z.array(z.string()).default([]),
127
+ });
128
+
129
+ // Extract defaults for form initialization
130
+ const defaults = getSchemaDefaults(userSchema);
131
+ // { age: 25, tags: [] }
132
+
133
+ // Check which fields require valid input (for showing * in forms)
134
+ requiresValidInput(userSchema.shape.name); // true - has min(1)
135
+ requiresValidInput(userSchema.shape.email); // true - email validation
136
+ requiresValidInput(userSchema.shape.age); // true - number type
137
+ requiresValidInput(userSchema.shape.bio); // false - optional
138
+
139
+ // Get validation constraints for UI hints
140
+ getFieldChecks(userSchema.shape.age);
141
+ // [
142
+ // { check: 'greater_than', value: 18, inclusive: true },
143
+ // { check: 'less_than', value: 120, inclusive: true }
144
+ // ]
145
+ ```
146
+
147
+ ---
31
148
 
32
149
  ## API Reference
33
150
 
34
- ### `getSchemaDefaults(schema)`
151
+ ### `getSchemaDefaults(schema, options?)`
35
152
 
36
153
  Extract all default values from a Zod object schema. Only extracts fields that explicitly have `.default()` on them.
37
154
 
38
- **Transform support:** Works with schemas that have `.transform()` - extracts defaults from the input type.
155
+ #### Basic Example
39
156
 
40
157
  ```typescript
41
158
  import { getSchemaDefaults } from "@zod-utils/core";
@@ -44,13 +161,8 @@ import { z } from "zod";
44
161
  const schema = z.object({
45
162
  name: z.string().default("John Doe"),
46
163
  age: z.number().default(25),
47
- email: z.string().email(), // no default - skipped
48
- settings: z
49
- .object({
50
- theme: z.string().default("light"),
51
- notifications: z.boolean().default(true),
52
- })
53
- .default({}), // must have explicit .default() to be extracted
164
+ email: z.string().email(), // no default - NOT included
165
+ active: z.boolean().default(true),
54
166
  tags: z.array(z.string()).default([]),
55
167
  });
56
168
 
@@ -58,236 +170,571 @@ const defaults = getSchemaDefaults(schema);
58
170
  // {
59
171
  // name: 'John Doe',
60
172
  // age: 25,
61
- // settings: {},
173
+ // active: true,
62
174
  // tags: []
63
175
  // }
64
176
  ```
65
177
 
66
- **Important:** Only fields with explicit `.default()` are extracted. Nested object fields without an explicit default on the parent field are not extracted, even if they contain defaults internally.
178
+ #### With Optional and Nullable Fields
67
179
 
68
- **Handles:**
180
+ ```typescript
181
+ const schema = z.object({
182
+ // Optional with default - included
183
+ nickname: z.string().default("Anonymous").optional(),
184
+ // Nullable with default - included
185
+ title: z.string().default("Mr.").nullable(),
186
+ // Optional without default - NOT included
187
+ middleName: z.string().optional(),
188
+ // Nullable without default - NOT included
189
+ suffix: z.string().nullable(),
190
+ });
69
191
 
70
- - Optional fields with defaults: `.optional().default(value)`
71
- - Nullable fields with defaults: `.nullable().default(value)`
72
- - Arrays with defaults: `.array().default([])`
73
- - Objects with defaults: `.object({...}).default({})`
74
- - Skips fields without explicit defaults
192
+ const defaults = getSchemaDefaults(schema);
193
+ // { nickname: 'Anonymous', title: 'Mr.' }
194
+ ```
75
195
 
76
- ---
196
+ #### With Function Defaults
77
197
 
78
- ### `requiresValidInput(field)`
198
+ ```typescript
199
+ const schema = z.object({
200
+ id: z.string().default(() => crypto.randomUUID()),
201
+ createdAt: z.number().default(() => Date.now()),
202
+ });
79
203
 
80
- Determines if a field will show validation errors when the user submits empty or invalid input. Useful for form UIs to show which fields need valid user input (asterisks, validation indicators).
204
+ const defaults = getSchemaDefaults(schema);
205
+ // { id: 'a1b2c3...', createdAt: 1703980800000 }
206
+ // Functions are called at extraction time
207
+ ```
81
208
 
82
- **Key insight:** Defaults are just initial values - they don't prevent validation errors if the user clears the field.
209
+ #### Nested Objects - Important Behavior
210
+
211
+ ```typescript
212
+ // Nested defaults are NOT extracted unless parent has .default()
213
+ const schema = z.object({
214
+ name: z.string().default("John"),
215
+ settings: z.object({
216
+ theme: z.string().default("light"), // Has default
217
+ notifications: z.boolean().default(true), // Has default
218
+ }), // Parent has NO .default() - entire object skipped!
219
+ });
220
+
221
+ getSchemaDefaults(schema);
222
+ // { name: 'John' }
223
+ // settings is NOT included because parent object has no .default()
224
+
225
+ // To include nested defaults, add .default() to parent:
226
+ const schemaWithNestedDefaults = z.object({
227
+ name: z.string().default("John"),
228
+ settings: z
229
+ .object({
230
+ theme: z.string().default("light"),
231
+ notifications: z.boolean().default(true),
232
+ })
233
+ .default({ theme: "light", notifications: true }), // Parent has .default()
234
+ });
235
+
236
+ getSchemaDefaults(schemaWithNestedDefaults);
237
+ // { name: 'John', settings: { theme: 'light', notifications: true } }
238
+ ```
83
239
 
84
- **Real-world example:**
240
+ #### With Discriminated Unions
85
241
 
86
242
  ```typescript
87
- // Marital status with default but validation rules
88
- const maritalStatus = z.string().min(1).default('single');
243
+ const formSchema = z.discriminatedUnion("mode", [
244
+ z.object({
245
+ mode: z.literal("create"),
246
+ name: z.string(),
247
+ age: z.number().default(18),
248
+ }),
249
+ z.object({
250
+ mode: z.literal("edit"),
251
+ id: z.number().default(1),
252
+ bio: z.string().default("bio goes here"),
253
+ }),
254
+ ]);
255
+
256
+ // Get defaults for 'create' mode
257
+ const createDefaults = getSchemaDefaults(formSchema, {
258
+ discriminator: { key: "mode", value: "create" },
259
+ });
260
+ // { age: 18 }
89
261
 
90
- // What happens in the form:
91
- // 1. Initial: field shows "single" (from default)
92
- // 2. User deletes the value empty string
93
- // 3. User submits form → validation fails (.min(1) rejects empty)
94
- // 4. requiresValidInput(maritalStatus) true (show *, show error)
262
+ // Get defaults for 'edit' mode
263
+ const editDefaults = getSchemaDefaults(formSchema, {
264
+ discriminator: { key: "mode", value: "edit" },
265
+ });
266
+ // { id: 1, bio: 'bio goes here' }
267
+
268
+ // Without discriminator, returns empty object
269
+ getSchemaDefaults(formSchema);
270
+ // {}
95
271
  ```
96
272
 
97
- **How it works:**
273
+ #### With Schema Transforms
274
+
275
+ ```typescript
276
+ const schema = z
277
+ .object({
278
+ name: z.string().default("John"),
279
+ age: z.number().default(25),
280
+ })
281
+ .transform((data) => ({ ...data, computed: true }));
282
+
283
+ // Extracts defaults from the INPUT type (before transform)
284
+ getSchemaDefaults(schema);
285
+ // { name: 'John', age: 25 }
286
+ ```
287
+
288
+ ---
289
+
290
+ ### `requiresValidInput(field)`
291
+
292
+ Determines if a field will show validation errors when the user submits empty or invalid input. Useful for form UIs to show which fields need valid user input (asterisks, validation indicators).
293
+
294
+ **Key insight:** Defaults are just initial values - they don't prevent validation errors if the user clears the field.
295
+
296
+ #### How It Works
98
297
 
99
298
  1. Removes `.default()` wrappers (defaults ≠ validation rules)
100
299
  2. Tests if underlying schema accepts empty/invalid input:
101
300
  - `undefined` (via `.optional()`)
102
301
  - `null` (via `.nullable()`)
103
- - Empty string (plain `z.string()`)
104
- - Empty array (plain `z.array()`)
302
+ - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)
303
+ - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)
105
304
  3. Returns `true` if validation will fail on empty input
106
305
 
107
- **Examples:**
306
+ #### String Fields
108
307
 
109
308
  ```typescript
110
309
  import { requiresValidInput } from "@zod-utils/core";
111
310
  import { z } from "zod";
112
311
 
113
- // User name - required, no default
114
- const userName = z.string().min(1);
115
- requiresValidInput(userName); // true - will error if empty
312
+ // Plain string accepts empty string ""
313
+ requiresValidInput(z.string()); // false
314
+ requiresValidInput(z.string().optional()); // false
315
+ requiresValidInput(z.string().nullable()); // false
316
+
317
+ // String with min(1) requires non-empty
318
+ requiresValidInput(z.string().min(1)); // true
319
+ requiresValidInput(z.string().nonempty()); // true
320
+
321
+ // Email validation rejects empty string
322
+ requiresValidInput(z.string().email()); // true
323
+
324
+ // Default doesn't change validation requirement!
325
+ requiresValidInput(z.string().default("hello")); // false - plain string
326
+ requiresValidInput(z.string().min(1).default("hello")); // true - has min(1)
327
+ ```
328
+
329
+ #### Number Fields
330
+
331
+ ```typescript
332
+ // Numbers always require valid input (empty string fails)
333
+ requiresValidInput(z.number()); // true
334
+ requiresValidInput(z.number().default(0)); // true - default doesn't help
335
+
336
+ // Optional numbers don't require input
337
+ requiresValidInput(z.number().optional()); // false
338
+ requiresValidInput(z.number().nullable()); // false
339
+ ```
340
+
341
+ #### Boolean Fields
342
+
343
+ ```typescript
344
+ // Booleans require true/false value
345
+ requiresValidInput(z.boolean()); // true
346
+ requiresValidInput(z.boolean().default(false)); // true
347
+
348
+ // Optional booleans don't require input
349
+ requiresValidInput(z.boolean().optional()); // false
350
+ ```
351
+
352
+ #### Array Fields
353
+
354
+ ```typescript
355
+ // Plain arrays accept empty []
356
+ requiresValidInput(z.array(z.string())); // false
357
+ requiresValidInput(z.array(z.string()).default([])); // false
358
+
359
+ // Arrays with min(1) require at least one item
360
+ requiresValidInput(z.array(z.string()).min(1)); // true
361
+ requiresValidInput(z.array(z.string()).nonempty()); // true
362
+
363
+ // Optional arrays don't require input
364
+ requiresValidInput(z.array(z.string()).optional()); // false
365
+ ```
366
+
367
+ #### Object Fields
368
+
369
+ ```typescript
370
+ // Objects require some structure
371
+ requiresValidInput(z.object({ name: z.string() })); // true
372
+
373
+ // Optional objects don't require input
374
+ requiresValidInput(z.object({ name: z.string() }).optional()); // false
375
+ ```
376
+
377
+ #### Other Types
378
+
379
+ ```typescript
380
+ // Enums and literals require exact match
381
+ requiresValidInput(z.enum(["a", "b", "c"])); // true
382
+ requiresValidInput(z.literal("test")); // true
383
+
384
+ // Any and unknown accept everything
385
+ requiresValidInput(z.any()); // false
386
+ requiresValidInput(z.unknown()); // false
387
+
388
+ // Never rejects everything
389
+ requiresValidInput(z.never()); // true
390
+ ```
391
+
392
+ #### Real-World Form Example
393
+
394
+ ```typescript
395
+ const userFormSchema = z.object({
396
+ // Required - show asterisk
397
+ firstName: z.string().min(1), // requiresValidInput: true
398
+ lastName: z.string().min(1), // requiresValidInput: true
399
+ email: z.string().email(), // requiresValidInput: true
400
+ age: z.number().min(18), // requiresValidInput: true
401
+
402
+ // Optional - no asterisk
403
+ middleName: z.string().optional(), // requiresValidInput: false
404
+ nickname: z.string(), // requiresValidInput: false (empty allowed)
405
+ bio: z.string().optional(), // requiresValidInput: false
406
+
407
+ // Has default but still requires valid input if user clears
408
+ role: z.string().min(1).default("user"), // requiresValidInput: true
409
+ });
410
+
411
+ // Use in form UI
412
+ function FormField({ name, schema }: { name: string; schema: z.ZodType }) {
413
+ const field = schema.shape[name];
414
+ const isRequired = requiresValidInput(field);
415
+
416
+ return (
417
+ <label>
418
+ {name} {isRequired && <span className="text-red-500">*</span>}
419
+ <input name={name} />
420
+ </label>
421
+ );
422
+ }
423
+ ```
424
+
425
+ ---
426
+
427
+ ### `extractDefaultValue(field)`
428
+
429
+ Extract the default value from a Zod field. Recursively unwraps optional/nullable/union/transform layers.
430
+
431
+ #### Basic Usage
432
+
433
+ ```typescript
434
+ import { extractDefaultValue } from "@zod-utils/core";
435
+ import { z } from "zod";
436
+
437
+ // Simple defaults
438
+ extractDefaultValue(z.string().default("hello")); // 'hello'
439
+ extractDefaultValue(z.number().default(42)); // 42
440
+ extractDefaultValue(z.boolean().default(true)); // true
441
+
442
+ // No default returns undefined
443
+ extractDefaultValue(z.string()); // undefined
444
+ extractDefaultValue(z.string().optional()); // undefined
445
+ ```
446
+
447
+ #### With Wrappers
448
+
449
+ ```typescript
450
+ // Unwraps optional/nullable to find default
451
+ extractDefaultValue(z.string().default("hello").optional()); // 'hello'
452
+ extractDefaultValue(z.string().default("hello").nullable()); // 'hello'
453
+ extractDefaultValue(z.string().default("hello").optional().nullable()); // 'hello'
454
+ ```
455
+
456
+ #### With Function Defaults
457
+
458
+ ```typescript
459
+ // Functions are called to get the value
460
+ extractDefaultValue(z.string().default(() => "dynamic")); // 'dynamic'
461
+ extractDefaultValue(z.number().default(() => Date.now())); // 1703980800000
462
+ ```
463
+
464
+ #### With Transforms
116
465
 
117
- // Marital status - required WITH default
118
- const maritalStatus = z.string().min(1).default('single');
119
- requiresValidInput(maritalStatus); // true - will error if user clears it
466
+ ```typescript
467
+ // Extracts input default, not output
468
+ const schema = z
469
+ .string()
470
+ .default("hello")
471
+ .transform((val) => val.toUpperCase());
120
472
 
121
- // Age with default - requires valid input
122
- const age = z.number().default(0);
123
- requiresValidInput(age); // true - numbers reject empty strings
473
+ extractDefaultValue(schema); // 'hello' (not 'HELLO')
474
+ ```
124
475
 
125
- // Optional bio - doesn't require input
126
- const bio = z.string().optional();
127
- requiresValidInput(bio); // false - user can leave empty
476
+ #### With Unions
128
477
 
129
- // Notes with default but NO validation
130
- const notes = z.string().default('N/A');
131
- requiresValidInput(notes); // false - plain z.string() accepts empty
478
+ ```typescript
479
+ // Only checks first option in union
480
+ extractDefaultValue(z.union([z.string().default("hello"), z.number()]));
481
+ // undefined - unions with multiple non-nullish types return undefined
132
482
 
133
- // Nullable middle name
134
- const middleName = z.string().nullable();
135
- requiresValidInput(middleName); // false - user can leave null
483
+ extractDefaultValue(z.union([z.string().default("hello"), z.null()]));
484
+ // 'hello' - union with nullish types extracts from first option
136
485
  ```
137
486
 
138
487
  ---
139
488
 
140
489
  ### `getPrimitiveType(field)`
141
490
 
142
- Get the primitive type of a Zod field by unwrapping optional/nullable/transform wrappers.
143
- Stops at arrays without unwrapping them.
491
+ Get the primitive type of a Zod field by unwrapping optional/nullable/default/transform wrappers. Stops at arrays without unwrapping them.
144
492
 
145
- **Transform support:** Automatically unwraps `.transform()` to get the underlying input type.
493
+ #### Basic Usage
146
494
 
147
495
  ```typescript
148
496
  import { getPrimitiveType } from "@zod-utils/core";
149
497
  import { z } from "zod";
150
498
 
151
- const field = z.string().optional().nullable();
152
- const primitive = getPrimitiveType(field);
153
- // Returns the underlying string schema
499
+ // Unwraps to get underlying type
500
+ getPrimitiveType(z.string()); // ZodString
501
+ getPrimitiveType(z.string().optional()); // ZodString
502
+ getPrimitiveType(z.string().nullable()); // ZodString
503
+ getPrimitiveType(z.string().default("test")); // ZodString
504
+ getPrimitiveType(z.string().optional().nullable()); // ZodString
505
+ getPrimitiveType(z.string().default("x").optional().nullable()); // ZodString
506
+ ```
507
+
508
+ #### Stops at Arrays
509
+
510
+ ```typescript
511
+ // Arrays are returned as-is (not unwrapped to element type)
512
+ getPrimitiveType(z.array(z.string())); // ZodArray
513
+ getPrimitiveType(z.array(z.string()).optional()); // ZodArray
514
+ ```
515
+
516
+ #### With Transforms
517
+
518
+ ```typescript
519
+ // Unwraps transform to get input type
520
+ getPrimitiveType(z.string().transform((val) => val.toUpperCase())); // ZodString
521
+
522
+ getPrimitiveType(
523
+ z.object({ name: z.string() }).transform((data) => ({ ...data, id: 1 }))
524
+ ); // ZodObject
525
+ ```
526
+
527
+ #### With Unions
154
528
 
155
- const arrayField = z.array(z.string()).optional();
156
- const arrayPrimitive = getPrimitiveType(arrayField);
157
- // Returns the ZodArray (stops at arrays)
529
+ ```typescript
530
+ // Union with only nullish types - extracts the non-nullish type
531
+ getPrimitiveType(z.union([z.string(), z.null()])); // ZodString
532
+ getPrimitiveType(z.union([z.number(), z.undefined()])); // ZodNumber
158
533
 
159
- // Transform support
160
- const transformed = z.string().transform((val) => val.toUpperCase());
161
- const primitiveFromTransform = getPrimitiveType(transformed);
162
- // Returns the underlying ZodString (unwraps the transform)
534
+ // Union with multiple non-nullish types - returns union as-is
535
+ getPrimitiveType(z.union([z.string(), z.number()])); // ZodUnion
536
+ getPrimitiveType(z.union([z.literal("a"), z.literal("b")])); // ZodUnion
163
537
  ```
164
538
 
165
539
  ---
166
540
 
167
541
  ### `removeDefault(field)`
168
542
 
169
- Remove default values from a Zod field.
543
+ Remove default values from a Zod field while preserving optional/nullable wrappers.
170
544
 
171
545
  ```typescript
172
546
  import { removeDefault } from "@zod-utils/core";
173
547
  import { z } from "zod";
174
548
 
549
+ // Remove default
175
550
  const withDefault = z.string().default("hello");
176
551
  const withoutDefault = removeDefault(withDefault);
177
552
 
178
553
  withDefault.parse(undefined); // 'hello'
179
- withoutDefault.parse(undefined); // throws error
554
+ withoutDefault.parse(undefined); // throws ZodError
555
+
556
+ // Preserves optional/nullable
557
+ const complex = z.string().default("test").optional().nullable();
558
+ const removed = removeDefault(complex);
559
+
560
+ removed.parse(undefined); // undefined (optional still works)
561
+ removed.parse(null); // null (nullable still works)
562
+ removed.parse("hello"); // 'hello'
563
+
564
+ // Returns same schema if no default
565
+ const plain = z.string();
566
+ removeDefault(plain) === plain; // true
180
567
  ```
181
568
 
182
569
  ---
183
570
 
184
- ### `extractDefaultValue(field)`
185
-
186
- Extract the default value from a Zod field (recursively unwraps optional/nullable/union/transform layers).
571
+ ### `getFieldChecks(field)`
187
572
 
188
- **Union handling:** For union types, extracts the default from the first option. If the first option has no default, returns `undefined` (defaults in other union options are not checked).
573
+ Extract all validation check definitions from a Zod schema field. Returns Zod's raw check definition objects.
189
574
 
190
- **Transform support:** Automatically unwraps `.transform()` to get the input type's default value.
575
+ #### String Validations
191
576
 
192
577
  ```typescript
193
- import { extractDefaultValue } from "@zod-utils/core";
578
+ import { getFieldChecks } from "@zod-utils/core";
194
579
  import { z } from "zod";
195
580
 
196
- // Basic usage
197
- const field = z.string().optional().default("hello");
198
- extractDefaultValue(field); // 'hello'
581
+ // Length constraints
582
+ getFieldChecks(z.string().min(3));
583
+ // [{ check: 'min_length', minimum: 3, ... }]
199
584
 
200
- const noDefault = z.string();
201
- extractDefaultValue(noDefault); // undefined
585
+ getFieldChecks(z.string().max(100));
586
+ // [{ check: 'max_length', maximum: 100, ... }]
202
587
 
203
- // Union with default in first option
204
- const unionField = z.union([z.string().default('hello'), z.number()]);
205
- extractDefaultValue(unionField); // 'hello'
588
+ getFieldChecks(z.string().min(3).max(20));
589
+ // [
590
+ // { check: 'min_length', minimum: 3, ... },
591
+ // { check: 'max_length', maximum: 20, ... }
592
+ // ]
206
593
 
207
- // Union with default in second option (only checks first)
208
- const unionField2 = z.union([z.string(), z.number().default(42)]);
209
- extractDefaultValue(unionField2); // undefined
594
+ getFieldChecks(z.string().length(10));
595
+ // [{ check: 'length_equals', length: 10, ... }]
210
596
 
211
- // Union wrapped in optional
212
- const wrappedUnion = z.union([z.string().default('test'), z.number()]).optional();
213
- extractDefaultValue(wrappedUnion); // 'test'
597
+ // Format validations
598
+ getFieldChecks(z.string().email());
599
+ // [{ check: 'string_format', format: 'email', ... }]
214
600
 
215
- // Transform support - extracts input default, not output
216
- const transformed = z.string().default('hello').transform((val) => val.toUpperCase());
217
- extractDefaultValue(transformed); // 'hello' (not 'HELLO')
601
+ getFieldChecks(z.string().url());
602
+ // [{ check: 'string_format', format: 'url', ... }]
603
+
604
+ getFieldChecks(z.string().uuid());
605
+ // [{ check: 'string_format', format: 'uuid', ... }]
218
606
  ```
219
607
 
220
- ---
608
+ #### Number Validations
221
609
 
222
- ### `getFieldChecks(field)`
610
+ ```typescript
611
+ getFieldChecks(z.number().min(18));
612
+ // [{ check: 'greater_than', value: 18, inclusive: true, ... }]
223
613
 
224
- Extract all validation check definitions from a Zod schema field. Returns Zod's raw check definition objects directly, including all properties like `check`, `minimum`, `maximum`, `value`, `inclusive`, `format`, `pattern`, etc.
614
+ getFieldChecks(z.number().max(120));
615
+ // [{ check: 'less_than', value: 120, inclusive: true, ... }]
225
616
 
226
- **Automatically unwraps:** optional, nullable, and default layers. For unions, checks only the first option.
617
+ getFieldChecks(z.number().min(0).max(100));
618
+ // [
619
+ // { check: 'greater_than', value: 0, inclusive: true, ... },
620
+ // { check: 'less_than', value: 100, inclusive: true, ... }
621
+ // ]
227
622
 
228
- **Supported check types:** Returns any of 21 check types:
229
- - **Length checks**: `min_length`, `max_length`, `length_equals` (strings, arrays)
230
- - **Size checks**: `min_size`, `max_size`, `size_equals` (files, sets, maps)
231
- - **Numeric checks**: `greater_than`, `less_than`, `multiple_of`
232
- - **Format checks**: `number_format`, `bigint_format`, `string_format` (email, url, uuid, etc.)
233
- - **String pattern checks**: `regex`, `lowercase`, `uppercase`, `includes`, `starts_with`, `ends_with`
234
- - **Other checks**: `property`, `mime_type`, `overwrite`
623
+ getFieldChecks(z.number().gt(0)); // exclusive >
624
+ // [{ check: 'greater_than', value: 0, inclusive: false, ... }]
235
625
 
236
- ```typescript
237
- import { getFieldChecks } from "@zod-utils/core";
238
- import { z } from "zod";
626
+ getFieldChecks(z.number().lt(100)); // exclusive <
627
+ // [{ check: 'less_than', value: 100, inclusive: false, ... }]
628
+ ```
239
629
 
240
- // String with length constraints
241
- const username = z.string().min(3).max(20);
242
- const checks = getFieldChecks(username);
243
- // [
244
- // { check: 'min_length', minimum: 3, when: [Function], ... },
245
- // { check: 'max_length', maximum: 20, when: [Function], ... }
246
- // ]
630
+ #### Array Validations
247
631
 
248
- // Number with range constraints
249
- const age = z.number().min(18).max(120);
250
- const checks = getFieldChecks(age);
251
- // [
252
- // { check: 'greater_than', value: 18, inclusive: true, ... },
253
- // { check: 'less_than', value: 120, inclusive: true, ... }
254
- // ]
632
+ ```typescript
633
+ getFieldChecks(z.array(z.string()).min(1));
634
+ // [{ check: 'min_length', minimum: 1, ... }]
255
635
 
256
- // Array with item count constraints
257
- const tags = z.array(z.string()).min(1).max(5);
258
- const checks = getFieldChecks(tags);
636
+ getFieldChecks(z.array(z.string()).max(10));
637
+ // [{ check: 'max_length', maximum: 10, ... }]
638
+
639
+ getFieldChecks(z.array(z.string()).min(1).max(5));
259
640
  // [
260
641
  // { check: 'min_length', minimum: 1, ... },
261
642
  // { check: 'max_length', maximum: 5, ... }
262
643
  // ]
644
+ ```
263
645
 
264
- // String with format validation
265
- const email = z.string().email();
266
- const checks = getFieldChecks(email);
267
- // [{ check: 'string_format', format: 'email', ... }]
646
+ #### Date Validations
647
+
648
+ ```typescript
649
+ const minDate = new Date("2024-01-01");
650
+ const maxDate = new Date("2024-12-31");
268
651
 
269
- // Unwrapping optional/nullable/default layers
270
- const bio = z.string().min(10).max(500).optional();
271
- const checks = getFieldChecks(bio);
652
+ getFieldChecks(z.date().min(minDate));
653
+ // [{ check: 'greater_than', value: Date, inclusive: true, ... }]
654
+
655
+ getFieldChecks(z.date().max(maxDate));
656
+ // [{ check: 'less_than', value: Date, inclusive: true, ... }]
657
+
658
+ getFieldChecks(z.date().min(minDate).max(maxDate));
272
659
  // [
273
- // { check: 'min_length', minimum: 10, ... },
274
- // { check: 'max_length', maximum: 500, ... }
660
+ // { check: 'greater_than', value: Date, inclusive: true, ... },
661
+ // { check: 'less_than', value: Date, inclusive: true, ... }
275
662
  // ]
663
+ ```
664
+
665
+ #### Unwrapping Behavior
666
+
667
+ ```typescript
668
+ // Automatically unwraps optional/nullable/default
669
+ getFieldChecks(z.string().min(3).max(20).optional());
670
+ // [{ check: 'min_length', ... }, { check: 'max_length', ... }]
276
671
 
277
- // No checks
278
- const plainString = z.string();
279
- getFieldChecks(plainString); // []
672
+ getFieldChecks(z.number().min(0).max(100).nullable());
673
+ // [{ check: 'greater_than', ... }, { check: 'less_than', ... }]
674
+
675
+ getFieldChecks(z.string().min(5).max(50).default("test"));
676
+ // [{ check: 'min_length', ... }, { check: 'max_length', ... }]
677
+
678
+ getFieldChecks(
679
+ z.string().min(10).max(500).optional().nullable().default("test")
680
+ );
681
+ // [{ check: 'min_length', ... }, { check: 'max_length', ... }]
280
682
  ```
281
683
 
282
- **Type:** The return type is `ZodUnionCheck[]`, a union of all 21 Zod check definition types. You can also import the `ZodUnionCheck` type:
684
+ #### No Constraints
283
685
 
284
686
  ```typescript
285
- import { getFieldChecks, type ZodUnionCheck } from "@zod-utils/core";
687
+ // Returns empty array when no constraints
688
+ getFieldChecks(z.string()); // []
689
+ getFieldChecks(z.number()); // []
690
+ getFieldChecks(z.boolean()); // []
691
+ getFieldChecks(z.date()); // []
692
+ ```
693
+
694
+ #### Using Checks in UI
695
+
696
+ ```typescript
697
+ function getInputProps(field: z.ZodType) {
698
+ const checks = getFieldChecks(field);
699
+ const props: Record<string, unknown> = {};
700
+
701
+ for (const check of checks) {
702
+ switch (check.check) {
703
+ case "min_length":
704
+ props.minLength = check.minimum;
705
+ break;
706
+ case "max_length":
707
+ props.maxLength = check.maximum;
708
+ break;
709
+ case "greater_than":
710
+ props.min = check.value;
711
+ break;
712
+ case "less_than":
713
+ props.max = check.value;
714
+ break;
715
+ case "string_format":
716
+ if (check.format === "email") props.type = "email";
717
+ if (check.format === "url") props.type = "url";
718
+ break;
719
+ }
720
+ }
721
+
722
+ return props;
723
+ }
724
+
725
+ // Usage
726
+ const usernameField = z.string().min(3).max(20);
727
+ getInputProps(usernameField);
728
+ // { minLength: 3, maxLength: 20 }
729
+
730
+ const ageField = z.number().min(18).max(120);
731
+ getInputProps(ageField);
732
+ // { min: 18, max: 120 }
286
733
  ```
287
734
 
288
735
  ---
289
736
 
290
- ### `extractDiscriminatedSchema(schema, key, value)`
737
+ ### `extractDiscriminatedSchema(props)`
291
738
 
292
739
  Extract a specific variant from a discriminated union schema based on the discriminator field and value.
293
740
 
@@ -295,14 +742,14 @@ Extract a specific variant from a discriminated union schema based on the discri
295
742
  import { extractDiscriminatedSchema } from "@zod-utils/core";
296
743
  import { z } from "zod";
297
744
 
298
- const userSchema = z.discriminatedUnion('mode', [
745
+ const userSchema = z.discriminatedUnion("mode", [
299
746
  z.object({
300
- mode: z.literal('create'),
747
+ mode: z.literal("create"),
301
748
  name: z.string(),
302
749
  age: z.number().optional(),
303
750
  }),
304
751
  z.object({
305
- mode: z.literal('edit'),
752
+ mode: z.literal("edit"),
306
753
  id: z.number(),
307
754
  name: z.string().optional(),
308
755
  bio: z.string().optional(),
@@ -312,195 +759,242 @@ const userSchema = z.discriminatedUnion('mode', [
312
759
  // Extract the 'create' variant
313
760
  const createSchema = extractDiscriminatedSchema({
314
761
  schema: userSchema,
315
- key: 'mode',
316
- value: 'create',
762
+ key: "mode",
763
+ value: "create",
317
764
  });
318
765
  // Returns: z.ZodObject with { mode, name, age }
319
766
 
320
767
  // Extract the 'edit' variant
321
768
  const editSchema = extractDiscriminatedSchema({
322
769
  schema: userSchema,
323
- key: 'mode',
324
- value: 'edit',
770
+ key: "mode",
771
+ value: "edit",
325
772
  });
326
773
  // Returns: z.ZodObject with { mode, id, name, bio }
774
+
775
+ // Invalid value returns undefined
776
+ const invalid = extractDiscriminatedSchema({
777
+ schema: userSchema,
778
+ key: "mode",
779
+ value: "invalid" as any,
780
+ });
781
+ // Returns: undefined
327
782
  ```
328
783
 
329
- **Use with discriminated unions:** This is essential when working with `z.discriminatedUnion()` schemas, as it extracts the correct variant schema based on the discriminator value.
784
+ #### With Different Discriminator Types
785
+
786
+ ```typescript
787
+ // Boolean discriminator
788
+ const responseSchema = z.discriminatedUnion("success", [
789
+ z.object({ success: z.literal(true), data: z.string() }),
790
+ z.object({ success: z.literal(false), error: z.string() }),
791
+ ]);
792
+
793
+ extractDiscriminatedSchema({
794
+ schema: responseSchema,
795
+ key: "success",
796
+ value: true,
797
+ });
798
+ // Returns: z.ZodObject with { success, data }
799
+
800
+ // Numeric discriminator
801
+ const statusSchema = z.discriminatedUnion("code", [
802
+ z.object({ code: z.literal(200), message: z.string() }),
803
+ z.object({ code: z.literal(404), error: z.string() }),
804
+ ]);
805
+
806
+ extractDiscriminatedSchema({
807
+ schema: statusSchema,
808
+ key: "code",
809
+ value: 200,
810
+ });
811
+ // Returns: z.ZodObject with { code, message }
812
+ ```
330
813
 
331
814
  ---
332
815
 
333
- ### `extractFieldFromSchema(schema, name, discriminator?)`
816
+ ### `extractFieldFromSchema(props)`
334
817
 
335
- Extract a single field from a Zod object or discriminated union schema.
818
+ Extract a single field from a Zod object or discriminated union schema. Supports dot-notation paths for nested fields.
336
819
 
337
- **Transform support:** Works with schemas that have `.transform()` - extracts fields from the input type.
820
+ #### Basic Usage
338
821
 
339
822
  ```typescript
340
823
  import { extractFieldFromSchema } from "@zod-utils/core";
341
824
  import { z } from "zod";
342
825
 
343
- // Simple object schema
344
826
  const userSchema = z.object({
345
827
  name: z.string(),
346
828
  age: z.number(),
347
829
  email: z.string().email(),
348
830
  });
349
831
 
350
- const nameField = extractFieldFromSchema({
351
- schema: userSchema,
352
- name: 'name',
832
+ extractFieldFromSchema({ schema: userSchema, name: "name" }); // ZodString
833
+ extractFieldFromSchema({ schema: userSchema, name: "age" }); // ZodNumber
834
+ extractFieldFromSchema({ schema: userSchema, name: "email" }); // ZodString
835
+ ```
836
+
837
+ #### Nested Paths
838
+
839
+ ```typescript
840
+ const schema = z.object({
841
+ user: z.object({
842
+ profile: z.object({
843
+ name: z.string(),
844
+ age: z.number(),
845
+ }),
846
+ }),
353
847
  });
354
- // Returns: ZodString
355
848
 
356
- // Discriminated union schema
357
- const formSchema = z.discriminatedUnion('mode', [
849
+ extractFieldFromSchema({ schema, name: "user" }); // ZodObject
850
+ extractFieldFromSchema({ schema, name: "user.profile" }); // ZodObject
851
+ extractFieldFromSchema({ schema, name: "user.profile.name" }); // ZodString
852
+ extractFieldFromSchema({ schema, name: "user.profile.age" }); // ZodNumber
853
+ ```
854
+
855
+ #### Array Element Access
856
+
857
+ ```typescript
858
+ const schema = z.object({
859
+ addresses: z.array(
860
+ z.object({
861
+ street: z.string(),
862
+ city: z.string(),
863
+ })
864
+ ),
865
+ });
866
+
867
+ // Use numeric index to access array elements
868
+ extractFieldFromSchema({ schema, name: "addresses.0.street" }); // ZodString
869
+ extractFieldFromSchema({ schema, name: "addresses.0.city" }); // ZodString
870
+ extractFieldFromSchema({ schema, name: "addresses.99.street" }); // ZodString (any index works)
871
+ ```
872
+
873
+ #### With Discriminated Unions
874
+
875
+ ```typescript
876
+ const formSchema = z.discriminatedUnion("mode", [
358
877
  z.object({
359
- mode: z.literal('create'),
878
+ mode: z.literal("create"),
360
879
  name: z.string(),
361
880
  age: z.number().optional(),
362
881
  }),
363
882
  z.object({
364
- mode: z.literal('edit'),
883
+ mode: z.literal("edit"),
365
884
  id: z.number(),
366
885
  name: z.string().optional(),
367
886
  }),
368
887
  ]);
369
888
 
370
- // Extract field from specific variant
371
- const idField = extractFieldFromSchema({
889
+ // Must provide discriminator for discriminated unions
890
+ extractFieldFromSchema({
372
891
  schema: formSchema,
373
- name: 'id',
374
- discriminator: {
375
- key: 'mode',
376
- value: 'edit',
377
- },
378
- });
379
- // Returns: ZodNumber
892
+ name: "name",
893
+ discriminator: { key: "mode", value: "create" },
894
+ }); // ZodString
380
895
 
381
- // Without discriminator on discriminated union, returns undefined
382
- const fieldWithoutDiscriminator = extractFieldFromSchema({
896
+ extractFieldFromSchema({
383
897
  schema: formSchema,
384
- name: 'name',
385
- });
386
- // Returns: undefined (need discriminator to know which variant)
898
+ name: "id",
899
+ discriminator: { key: "mode", value: "edit" },
900
+ }); // ZodNumber
901
+
902
+ // Without discriminator, returns undefined
903
+ extractFieldFromSchema({
904
+ schema: formSchema,
905
+ name: "name",
906
+ }); // undefined
907
+ ```
387
908
 
388
- // Works with transforms - extracts from input type
389
- const transformedSchema = z
909
+ #### With Transforms
910
+
911
+ ```typescript
912
+ const schema = z
390
913
  .object({
391
914
  name: z.string(),
392
915
  age: z.number(),
393
916
  })
394
917
  .transform((data) => ({ ...data, computed: true }));
395
918
 
396
- const nameFromTransformed = extractFieldFromSchema({
397
- schema: transformedSchema,
398
- name: 'name',
399
- });
400
- // Returns: ZodString (from the input type, not affected by transform)
919
+ // Extracts from input type (before transform)
920
+ extractFieldFromSchema({ schema, name: "name" }); // ZodString
921
+ extractFieldFromSchema({ schema, name: "age" }); // ZodNumber
401
922
  ```
402
923
 
403
- **Discriminated union support:** When extracting fields from discriminated unions, you must provide the `discriminator` option with `key` and `value` to specify which variant to use.
404
-
405
924
  ---
406
925
 
407
926
  ### `extendWithMeta(field, transform)`
408
927
 
409
928
  Extends a Zod field with a transformation while preserving its metadata.
410
929
 
411
- This is useful when you want to add validations or transformations to a field but keep the original metadata (like `translationKey`) intact.
412
-
413
930
  ```typescript
414
931
  import { extendWithMeta } from "@zod-utils/core";
415
932
  import { z } from "zod";
416
933
 
417
934
  // Base field with metadata
418
- const baseField = z.string().meta({ translationKey: 'user.field.name' });
935
+ const baseField = z.string().meta({ translationKey: "user.field.name" });
419
936
 
420
937
  // Extend with validation while keeping metadata
421
938
  const extendedField = extendWithMeta(baseField, (f) => f.min(3).max(100));
422
939
 
423
940
  extendedField.meta(); // { translationKey: 'user.field.name' }
424
-
425
- // Validation still works
426
- extendedField.parse('ab'); // throws - too short
427
- extendedField.parse('abc'); // 'abc' - valid
941
+ extendedField.parse("ab"); // throws - too short
942
+ extendedField.parse("abc"); // 'abc' - valid
428
943
  ```
429
944
 
430
- **Use case:** When building forms with shared field definitions, you may want to reuse a base field with metadata across multiple schemas while adding schema-specific validations:
945
+ #### Use Case: Shared Field Definitions with i18n
431
946
 
432
947
  ```typescript
433
948
  // Shared field definitions with i18n metadata
434
949
  const fields = {
435
- name: z.string().meta({ translationKey: 'user.field.name' }),
436
- email: z.string().email().meta({ translationKey: 'user.field.email' }),
950
+ name: z.string().meta({ translationKey: "user.field.name" }),
951
+ email: z.string().email().meta({ translationKey: "user.field.email" }),
952
+ age: z.number().meta({ translationKey: "user.field.age" }),
437
953
  };
438
954
 
439
955
  // Create form uses base fields with additional constraints
440
956
  const createFormSchema = z.object({
441
957
  name: extendWithMeta(fields.name, (f) => f.min(3).max(50)),
442
958
  email: extendWithMeta(fields.email, (f) => f.min(5)),
959
+ age: extendWithMeta(fields.age, (f) => f.min(18).max(120)),
443
960
  });
444
961
 
445
962
  // Edit form uses same fields with different constraints
446
963
  const editFormSchema = z.object({
447
964
  name: extendWithMeta(fields.name, (f) => f.optional()),
448
965
  email: fields.email, // no extension needed
966
+ age: extendWithMeta(fields.age, (f) => f.optional()),
449
967
  });
450
968
  ```
451
969
 
452
- **Note:** If the original field has no metadata, the transformed field is returned as-is without calling `.meta()`.
453
-
454
970
  ---
455
971
 
456
- ### `getSchemaDefaults(schema, discriminator?)`
972
+ ### `toFieldSelector(props)`
457
973
 
458
- **Updated:** Now supports discriminated union schemas with the `discriminator` option.
974
+ Extracts a `FieldSelector` from props containing schema, name, and optional discriminator. Encapsulates type assertion so callers don't need eslint-disable.
459
975
 
460
976
  ```typescript
461
- import { getSchemaDefaults } from "@zod-utils/core";
462
- import { z } from "zod";
977
+ import { toFieldSelector } from "@zod-utils/core";
463
978
 
464
- // Discriminated union with defaults
465
- const formSchema = z.discriminatedUnion('mode', [
466
- z.object({
467
- mode: z.literal('create'),
468
- name: z.string(),
469
- age: z.number().default(18),
470
- }),
471
- z.object({
472
- mode: z.literal('edit'),
473
- id: z.number().default(1),
474
- name: z.string().optional(),
475
- bio: z.string().default('bio goes here'),
476
- }),
477
- ]);
979
+ const schema = z.object({ name: z.string(), age: z.number() });
478
980
 
479
- // Get defaults for 'create' mode
480
- const createDefaults = getSchemaDefaults(formSchema, {
481
- discriminator: {
482
- key: 'mode',
483
- value: 'create',
484
- },
485
- });
486
- // Returns: { age: 18 }
981
+ const selector = toFieldSelector({ schema, name: "name" });
982
+ // { schema, name: 'name' }
487
983
 
488
- // Get defaults for 'edit' mode
489
- const editDefaults = getSchemaDefaults(formSchema, {
490
- discriminator: {
491
- key: 'mode',
492
- value: 'edit',
493
- },
494
- });
495
- // Returns: { id: 1, bio: 'bio goes here' }
984
+ // With discriminated union
985
+ const unionSchema = z.discriminatedUnion("mode", [
986
+ z.object({ mode: z.literal("create"), title: z.string() }),
987
+ z.object({ mode: z.literal("edit"), id: z.number() }),
988
+ ]);
496
989
 
497
- // Without discriminator, returns empty object
498
- const noDefaults = getSchemaDefaults(formSchema);
499
- // Returns: {}
990
+ const unionSelector = toFieldSelector({
991
+ schema: unionSchema,
992
+ name: "title",
993
+ discriminator: { key: "mode", value: "create" },
994
+ });
995
+ // { schema: unionSchema, name: 'title', discriminator: { key: 'mode', value: 'create' } }
500
996
  ```
501
997
 
502
- **Discriminator types:** The discriminator `value` can be a string, number, or boolean literal that matches the discriminator field type.
503
-
504
998
  ---
505
999
 
506
1000
  ## Type Utilities
@@ -519,40 +1013,67 @@ type Simple = Simplify<Complex>;
519
1013
 
520
1014
  ---
521
1015
 
522
- ### `DiscriminatedInput<TSchema, TDiscriminatorKey, TDiscriminatorValue>`
1016
+ ### `Paths<T, FilterType?, Strict?>`
1017
+
1018
+ Generate dot-notation paths from any type. Low-level utility for building path strings.
523
1019
 
524
- Extracts the input type from a discriminated union variant. For discriminated unions, narrows to the variant matching the discriminator value and returns its input type. For regular schemas, returns the full input type.
1020
+ #### Basic Usage
525
1021
 
526
1022
  ```typescript
527
- import type { DiscriminatedInput } from "@zod-utils/core";
528
- import { z } from "zod";
1023
+ import type { Paths } from "@zod-utils/core";
529
1024
 
530
- const schema = z.discriminatedUnion("mode", [
531
- z.object({ mode: z.literal("create"), name: z.string() }),
532
- z.object({ mode: z.literal("edit"), id: z.number() }),
533
- ]);
1025
+ type User = {
1026
+ name: string;
1027
+ age: number;
1028
+ profile: {
1029
+ bio: string;
1030
+ avatar: string;
1031
+ };
1032
+ tags: string[];
1033
+ };
534
1034
 
535
- type CreateInput = DiscriminatedInput<typeof schema, "mode", "create">;
536
- // { mode: 'create'; name: string }
1035
+ // All paths
1036
+ type AllPaths = Paths<User>;
1037
+ // "name" | "age" | "profile" | "profile.bio" | "profile.avatar" | "tags" | "tags.0"
1038
+ ```
537
1039
 
538
- type EditInput = DiscriminatedInput<typeof schema, "mode", "edit">;
539
- // { mode: 'edit'; id: number }
1040
+ #### Filter by Type
1041
+
1042
+ ```typescript
1043
+ // Only string paths
1044
+ type StringPaths = Paths<User, string>;
1045
+ // "name" | "profile.bio" | "profile.avatar"
1046
+
1047
+ // Only number paths
1048
+ type NumberPaths = Paths<User, number>;
1049
+ // "age"
1050
+ ```
1051
+
1052
+ #### Strict vs Non-Strict Mode
1053
+
1054
+ ```typescript
1055
+ type Schema = {
1056
+ required: string;
1057
+ optional?: string;
1058
+ nullable: string | null;
1059
+ };
1060
+
1061
+ // Strict mode (default) - exact type matching
1062
+ type StrictPaths = Paths<Schema, string>;
1063
+ // "required" - only exact string type
1064
+
1065
+ // Non-strict mode - includes optional/nullable
1066
+ type LoosePaths = Paths<Schema, string, false>;
1067
+ // "required" | "optional" | "nullable"
540
1068
  ```
541
1069
 
542
1070
  ---
543
1071
 
544
1072
  ### `ValidPaths<TSchema, TDiscriminatorKey?, TDiscriminatorValue?, TFilterType?, TStrict?>`
545
1073
 
546
- Generates valid dot-notation paths for a schema, with optional type filtering and discriminated union support.
1074
+ Generates valid dot-notation paths for a Zod schema, with optional type filtering and discriminated union support.
547
1075
 
548
- **Parameters:**
549
- - `TSchema` - The Zod schema type
550
- - `TDiscriminatorKey` - Discriminator key for discriminated unions (default: `never`)
551
- - `TDiscriminatorValue` - Discriminator value to filter variant (default: `never`)
552
- - `TFilterType` - Type to filter paths by (default: `unknown` = all paths)
553
- - `TStrict` - Strict mode for type matching (default: `true`)
554
-
555
- **Basic usage:**
1076
+ #### Basic Usage
556
1077
 
557
1078
  ```typescript
558
1079
  import type { ValidPaths } from "@zod-utils/core";
@@ -565,20 +1086,24 @@ const schema = z.object({
565
1086
  active: z.boolean(),
566
1087
  });
567
1088
 
568
- // Get all paths (no filtering)
1089
+ // All paths
569
1090
  type AllPaths = ValidPaths<typeof schema>;
570
1091
  // "name" | "age" | "email" | "active"
1092
+ ```
1093
+
1094
+ #### Filter by Type
571
1095
 
572
- // Filter by type - only string fields
1096
+ ```typescript
1097
+ // Only string fields
573
1098
  type StringPaths = ValidPaths<typeof schema, never, never, string>;
574
1099
  // "name"
575
1100
 
576
1101
  // Non-strict mode - includes optional string fields
577
- type StringPathsNonStrict = ValidPaths<typeof schema, never, never, string, false>;
1102
+ type LooseStringPaths = ValidPaths<typeof schema, never, never, string, false>;
578
1103
  // "name" | "email"
579
1104
  ```
580
1105
 
581
- **With discriminated unions:**
1106
+ #### With Discriminated Unions
582
1107
 
583
1108
  ```typescript
584
1109
  const formSchema = z.discriminatedUnion("mode", [
@@ -586,55 +1111,300 @@ const formSchema = z.discriminatedUnion("mode", [
586
1111
  z.object({ mode: z.literal("edit"), id: z.number(), title: z.string() }),
587
1112
  ]);
588
1113
 
589
- // Get all paths for 'create' variant
1114
+ // All paths for 'create' variant
590
1115
  type CreatePaths = ValidPaths<typeof formSchema, "mode", "create">;
591
1116
  // "mode" | "name" | "age"
592
1117
 
593
- // Get number paths for 'edit' variant
1118
+ // All paths for 'edit' variant
1119
+ type EditPaths = ValidPaths<typeof formSchema, "mode", "edit">;
1120
+ // "mode" | "id" | "title"
1121
+
1122
+ // Number paths for 'edit' variant
594
1123
  type EditNumberPaths = ValidPaths<typeof formSchema, "mode", "edit", number>;
595
1124
  // "id"
1125
+
1126
+ // String paths for 'create' variant (non-strict to include optional)
1127
+ type CreateStringPaths = ValidPaths<typeof formSchema, "mode", "create", string, false>;
1128
+ // "name"
1129
+ ```
1130
+
1131
+ #### Nested Objects
1132
+
1133
+ ```typescript
1134
+ const schema = z.object({
1135
+ user: z.object({
1136
+ profile: z.object({
1137
+ firstName: z.string(),
1138
+ lastName: z.string(),
1139
+ age: z.number(),
1140
+ }),
1141
+ settings: z.object({
1142
+ theme: z.string(),
1143
+ notifications: z.boolean(),
1144
+ }),
1145
+ }),
1146
+ metadata: z.object({
1147
+ createdAt: z.date(),
1148
+ updatedAt: z.date(),
1149
+ }),
1150
+ });
1151
+
1152
+ // All paths including nested
1153
+ type AllPaths = ValidPaths<typeof schema>;
1154
+ // "user" | "user.profile" | "user.profile.firstName" | "user.profile.lastName" |
1155
+ // "user.profile.age" | "user.settings" | "user.settings.theme" |
1156
+ // "user.settings.notifications" | "metadata" | "metadata.createdAt" | "metadata.updatedAt"
1157
+
1158
+ // Only string paths (deeply nested)
1159
+ type StringPaths = ValidPaths<typeof schema, never, never, string>;
1160
+ // "user.profile.firstName" | "user.profile.lastName" | "user.settings.theme"
1161
+
1162
+ // Only boolean paths
1163
+ type BooleanPaths = ValidPaths<typeof schema, never, never, boolean>;
1164
+ // "user.settings.notifications"
1165
+
1166
+ // Only Date paths
1167
+ type DatePaths = ValidPaths<typeof schema, never, never, Date>;
1168
+ // "metadata.createdAt" | "metadata.updatedAt"
596
1169
  ```
597
1170
 
598
- **Strict vs Non-Strict mode:**
1171
+ #### Arrays and Array Elements
1172
+
1173
+ ```typescript
1174
+ const schema = z.object({
1175
+ tags: z.array(z.string()),
1176
+ scores: z.array(z.number()),
1177
+ users: z.array(
1178
+ z.object({
1179
+ id: z.number(),
1180
+ name: z.string(),
1181
+ email: z.string().optional(),
1182
+ })
1183
+ ),
1184
+ matrix: z.array(z.array(z.number())),
1185
+ });
1186
+
1187
+ // All paths - arrays use numeric index syntax
1188
+ type AllPaths = ValidPaths<typeof schema>;
1189
+ // "tags" | "tags.0" | "scores" | "scores.0" |
1190
+ // "users" | "users.0" | "users.0.id" | "users.0.name" | "users.0.email" |
1191
+ // "matrix" | "matrix.0" | "matrix.0.0"
1192
+
1193
+ // Array element paths only (filter by element type)
1194
+ type StringArrayElements = ValidPaths<typeof schema, never, never, string>;
1195
+ // "tags.0" | "users.0.name"
1196
+
1197
+ // Number array elements
1198
+ type NumberArrayElements = ValidPaths<typeof schema, never, never, number>;
1199
+ // "scores.0" | "users.0.id" | "matrix.0.0"
1200
+
1201
+ // The "0" is a placeholder - works with any numeric index at runtime
1202
+ // These are all valid at runtime: "users.0.name", "users.1.name", "users.99.name"
1203
+ ```
1204
+
1205
+ #### Optional Nested Objects and Arrays
1206
+
1207
+ ```typescript
1208
+ const schema = z.object({
1209
+ // Required nested object
1210
+ profile: z.object({
1211
+ name: z.string(),
1212
+ age: z.number(),
1213
+ }),
1214
+ // Optional nested object
1215
+ settings: z
1216
+ .object({
1217
+ theme: z.string(),
1218
+ language: z.string(),
1219
+ })
1220
+ .optional(),
1221
+ // Nullable nested object
1222
+ metadata: z
1223
+ .object({
1224
+ source: z.string(),
1225
+ version: z.number(),
1226
+ })
1227
+ .nullable(),
1228
+ // Required array
1229
+ tags: z.array(z.string()),
1230
+ // Optional array
1231
+ scores: z.array(z.number()).optional(),
1232
+ // Nullable array of objects
1233
+ comments: z
1234
+ .array(
1235
+ z.object({
1236
+ author: z.string(),
1237
+ text: z.string(),
1238
+ })
1239
+ )
1240
+ .nullable(),
1241
+ });
1242
+
1243
+ // All paths (no filter) - includes everything
1244
+ type AllPaths = ValidPaths<typeof schema>;
1245
+ // "profile" | "profile.name" | "profile.age" |
1246
+ // "settings" | "settings.theme" | "settings.language" |
1247
+ // "metadata" | "metadata.source" | "metadata.version" |
1248
+ // "tags" | "tags.0" |
1249
+ // "scores" | "scores.0" |
1250
+ // "comments" | "comments.0" | "comments.0.author" | "comments.0.text"
1251
+
1252
+ // Strict mode - paths through optional/nullable parents are BLOCKED
1253
+ type StrictStringPaths = ValidPaths<typeof schema, never, never, string>;
1254
+ // "profile.name" | "tags.0"
1255
+ // Note: settings.theme, metadata.source, comments.0.author are excluded
1256
+ // because their parent objects are optional/nullable
1257
+
1258
+ // Non-strict mode - paths through optional/nullable parents are ALLOWED
1259
+ type LooseStringPaths = ValidPaths<typeof schema, never, never, string, false>;
1260
+ // "profile.name" | "settings.theme" | "settings.language" |
1261
+ // "metadata.source" | "tags.0" | "comments.0.author" | "comments.0.text"
1262
+
1263
+ // Strict number paths
1264
+ type StrictNumberPaths = ValidPaths<typeof schema, never, never, number>;
1265
+ // "profile.age"
1266
+ // Note: metadata.version and scores.0 are excluded (optional/nullable parents)
1267
+
1268
+ // Non-strict number paths
1269
+ type LooseNumberPaths = ValidPaths<typeof schema, never, never, number, false>;
1270
+ // "profile.age" | "metadata.version" | "scores.0"
1271
+ ```
1272
+
1273
+ #### When to Use Strict vs Non-Strict Mode
1274
+
1275
+ ```typescript
1276
+ // Strict mode (default): Use when you need GUARANTEED non-null values
1277
+ // - Form fields that must always exist
1278
+ // - Required input components
1279
+ // - Direct property access without null checks
1280
+
1281
+ // Non-strict mode: Use when you handle optional/nullable at runtime
1282
+ // - Conditional form fields
1283
+ // - Fields that may or may not be rendered
1284
+ // - When you already check for existence before accessing
1285
+ ```
1286
+
1287
+ #### Practical Example: Type-Safe Form Field Component
1288
+
1289
+ ```typescript
1290
+ const userFormSchema = z.object({
1291
+ personal: z.object({
1292
+ firstName: z.string().min(1),
1293
+ lastName: z.string().min(1),
1294
+ age: z.number().min(18),
1295
+ }),
1296
+ contact: z.object({
1297
+ email: z.string().email(),
1298
+ phone: z.string().optional(),
1299
+ }),
1300
+ addresses: z.array(
1301
+ z.object({
1302
+ street: z.string(),
1303
+ city: z.string(),
1304
+ zipCode: z.string(),
1305
+ })
1306
+ ),
1307
+ });
1308
+
1309
+ // Type-safe field component that only accepts valid paths
1310
+ function TextField<TName extends ValidPaths<typeof userFormSchema, never, never, string>>({
1311
+ name,
1312
+ }: {
1313
+ name: TName;
1314
+ }) {
1315
+ // Implementation
1316
+ }
1317
+
1318
+ // Usage - TypeScript provides autocomplete for all string paths:
1319
+ <TextField name="personal.firstName" /> // ✅ Valid
1320
+ <TextField name="personal.lastName" /> // ✅ Valid
1321
+ <TextField name="contact.email" /> // ✅ Valid
1322
+ <TextField name="addresses.0.street" /> // ✅ Valid
1323
+ <TextField name="addresses.0.city" /> // ✅ Valid
1324
+
1325
+ // TypeScript errors:
1326
+ <TextField name="personal.age" /> // ❌ Error: age is number, not string
1327
+ <TextField name="invalid.path" /> // ❌ Error: path doesn't exist
1328
+
1329
+ // Number field component
1330
+ function NumberField<TName extends ValidPaths<typeof userFormSchema, never, never, number>>({
1331
+ name,
1332
+ }: {
1333
+ name: TName;
1334
+ }) {
1335
+ // Implementation
1336
+ }
1337
+
1338
+ <NumberField name="personal.age" /> // ✅ Valid - age is number
1339
+ <NumberField name="personal.firstName" /> // ❌ Error: firstName is string
1340
+ ```
1341
+
1342
+ #### Optional and Nullable Fields with Strict Mode
599
1343
 
600
1344
  ```typescript
601
1345
  const schema = z.object({
602
1346
  required: z.string(),
603
1347
  optional: z.string().optional(),
604
1348
  nullable: z.string().nullable(),
1349
+ optionalNullable: z.string().optional().nullable(),
1350
+ withDefault: z.string().default("hello"),
605
1351
  });
606
1352
 
607
- // Strict mode (default) - exact type matching
608
- type StrictPaths = ValidPaths<typeof schema, never, never, string>;
609
- // "required" - only exact string
1353
+ // Strict mode (default) - only exact type matches
1354
+ type StrictStringPaths = ValidPaths<typeof schema, never, never, string>;
1355
+ // "required" | "withDefault"
1356
+ // Note: optional/nullable are excluded because their type is string | undefined/null
610
1357
 
611
- // Non-strict mode - includes subtypes
612
- type NonStrictPaths = ValidPaths<typeof schema, never, never, string, false>;
613
- // "required" | "optional" | "nullable"
1358
+ // Non-strict mode - includes optional and nullable fields
1359
+ type LooseStringPaths = ValidPaths<typeof schema, never, never, string, false>;
1360
+ // "required" | "optional" | "nullable" | "optionalNullable" | "withDefault"
614
1361
  ```
615
1362
 
616
- ---
617
-
618
- ### `Paths<T, FilterType?, Strict?>`
619
-
620
- Low-level type utility for generating dot-notation paths from any type (not schema-specific).
1363
+ #### Complex Discriminated Union with Nested Arrays
621
1364
 
622
1365
  ```typescript
623
- import type { Paths } from "@zod-utils/core";
1366
+ const orderSchema = z.discriminatedUnion("type", [
1367
+ z.object({
1368
+ type: z.literal("physical"),
1369
+ items: z.array(
1370
+ z.object({
1371
+ productId: z.number(),
1372
+ quantity: z.number(),
1373
+ price: z.number(),
1374
+ })
1375
+ ),
1376
+ shippingAddress: z.object({
1377
+ street: z.string(),
1378
+ city: z.string(),
1379
+ country: z.string(),
1380
+ }),
1381
+ }),
1382
+ z.object({
1383
+ type: z.literal("digital"),
1384
+ items: z.array(
1385
+ z.object({
1386
+ productId: z.number(),
1387
+ licenseKey: z.string(),
1388
+ downloadUrl: z.string(),
1389
+ })
1390
+ ),
1391
+ email: z.string().email(),
1392
+ }),
1393
+ ]);
624
1394
 
625
- type User = {
626
- name: string;
627
- age: number;
628
- profile: { bio: string };
629
- };
1395
+ // Physical order paths
1396
+ type PhysicalPaths = ValidPaths<typeof orderSchema, "type", "physical">;
1397
+ // "type" | "items" | "items.0" | "items.0.productId" | "items.0.quantity" |
1398
+ // "items.0.price" | "shippingAddress" | "shippingAddress.street" |
1399
+ // "shippingAddress.city" | "shippingAddress.country"
630
1400
 
631
- // All paths
632
- type AllPaths = Paths<User>;
633
- // "name" | "age" | "profile" | "profile.bio"
1401
+ // Digital order string paths
1402
+ type DigitalStringPaths = ValidPaths<typeof orderSchema, "type", "digital", string>;
1403
+ // "items.0.licenseKey" | "items.0.downloadUrl" | "email"
634
1404
 
635
- // Filtered by string
636
- type StringPaths = Paths<User, string>;
637
- // "name" | "profile.bio"
1405
+ // Physical order number paths (for numeric inputs)
1406
+ type PhysicalNumberPaths = ValidPaths<typeof orderSchema, "type", "physical", number>;
1407
+ // "items.0.productId" | "items.0.quantity" | "items.0.price"
638
1408
  ```
639
1409
 
640
1410
  ---
@@ -663,54 +1433,61 @@ type FormParams = FieldSelector<typeof formSchema, "name", "mode", "create">;
663
1433
 
664
1434
  ---
665
1435
 
1436
+ ### `DiscriminatedInput<TSchema, TDiscriminatorKey, TDiscriminatorValue>`
1437
+
1438
+ Extracts the input type from a discriminated union variant.
1439
+
1440
+ ```typescript
1441
+ import type { DiscriminatedInput } from "@zod-utils/core";
1442
+ import { z } from "zod";
1443
+
1444
+ const schema = z.discriminatedUnion("mode", [
1445
+ z.object({ mode: z.literal("create"), name: z.string() }),
1446
+ z.object({ mode: z.literal("edit"), id: z.number() }),
1447
+ ]);
1448
+
1449
+ type CreateInput = DiscriminatedInput<typeof schema, "mode", "create">;
1450
+ // { mode: 'create'; name: string }
1451
+
1452
+ type EditInput = DiscriminatedInput<typeof schema, "mode", "edit">;
1453
+ // { mode: 'edit'; id: number }
1454
+ ```
1455
+
1456
+ ---
1457
+
666
1458
  ## Migration Guide
667
1459
 
668
1460
  ### Migrating to v3.0.0
669
1461
 
670
1462
  #### `ValidPathsOfType` removed → Use `ValidPaths` with type filtering
671
1463
 
672
- The `ValidPathsOfType` type has been removed and consolidated into `ValidPaths` with a new `TFilterType` parameter.
673
-
674
- **Before (v2.x):**
675
1464
  ```typescript
1465
+ // Before (v2.x)
676
1466
  import type { ValidPathsOfType } from "@zod-utils/core";
677
-
678
- // Get string field paths
679
1467
  type StringPaths = ValidPathsOfType<typeof schema, string>;
680
-
681
- // With discriminated union
682
1468
  type EditNumberPaths = ValidPathsOfType<typeof schema, number, "mode", "edit">;
683
- ```
684
1469
 
685
- **After (v3.x):**
686
- ```typescript
1470
+ // After (v3.x)
687
1471
  import type { ValidPaths } from "@zod-utils/core";
688
-
689
- // Get string field paths - use 4th type parameter
690
1472
  type StringPaths = ValidPaths<typeof schema, never, never, string>;
691
-
692
- // With discriminated union - TFilterType is now 4th parameter
693
1473
  type EditNumberPaths = ValidPaths<typeof schema, "mode", "edit", number>;
694
1474
  ```
695
1475
 
696
- #### `Paths<T>` signature changed
1476
+ ### Migrating to v4.0.0
697
1477
 
698
- The `Paths` type now accepts optional `FilterType` and `Strict` parameters.
1478
+ #### `mergeFieldSelectorProps` renamed Use `toFieldSelector`
699
1479
 
700
- **Before (v2.x):**
701
1480
  ```typescript
702
- type AllPaths = Paths<User>; // Still works the same
703
- ```
704
-
705
- **After (v3.x):**
706
- ```typescript
707
- type AllPaths = Paths<User>; // Works the same (backward compatible)
708
-
709
- // New: Filter by type
710
- type StringPaths = Paths<User, string>;
711
-
712
- // New: Non-strict mode
713
- type StringPaths = Paths<User, string, false>;
1481
+ // Before (v3.x)
1482
+ import { mergeFieldSelectorProps } from "@zod-utils/core";
1483
+ const selectorProps = mergeFieldSelectorProps(
1484
+ { schema },
1485
+ { name, discriminator }
1486
+ );
1487
+
1488
+ // After (v4.x)
1489
+ import { toFieldSelector } from "@zod-utils/core";
1490
+ const selectorProps = toFieldSelector({ schema, name, discriminator });
714
1491
  ```
715
1492
 
716
1493
  ---