@zod-utils/core 4.0.0 → 6.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 }
261
+
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' }
89
267
 
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)
268
+ // Without discriminator, returns empty object
269
+ getSchemaDefaults(formSchema);
270
+ // {}
271
+ ```
272
+
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 }
95
286
  ```
96
287
 
97
- **How it works:**
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
116
362
 
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
363
+ // Optional arrays don't require input
364
+ requiresValidInput(z.array(z.string()).optional()); // false
365
+ ```
366
+
367
+ #### Object Fields
120
368
 
121
- // Age with default - requires valid input
122
- const age = z.number().default(0);
123
- requiresValidInput(age); // true - numbers reject empty strings
369
+ ```typescript
370
+ // Objects require some structure
371
+ requiresValidInput(z.object({ name: z.string() })); // true
124
372
 
125
- // Optional bio - doesn't require input
126
- const bio = z.string().optional();
127
- requiresValidInput(bio); // false - user can leave empty
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
128
383
 
129
- // Notes with default but NO validation
130
- const notes = z.string().default('N/A');
131
- requiresValidInput(notes); // false - plain z.string() accepts empty
384
+ // Any and unknown accept everything
385
+ requiresValidInput(z.any()); // false
386
+ requiresValidInput(z.unknown()); // false
132
387
 
133
- // Nullable middle name
134
- const middleName = z.string().nullable();
135
- requiresValidInput(middleName); // false - user can leave null
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
+ }
136
423
  ```
137
424
 
138
425
  ---
139
426
 
140
- ### `getPrimitiveType(field)`
427
+ ### `extractDefaultValue(field)`
141
428
 
142
- Get the primitive type of a Zod field by unwrapping optional/nullable/transform wrappers.
143
- Stops at arrays without unwrapping them.
429
+ Extract the default value from a Zod field. Recursively unwraps optional/nullable/union/transform layers.
144
430
 
145
- **Transform support:** Automatically unwraps `.transform()` to get the underlying input type.
431
+ #### Basic Usage
146
432
 
147
433
  ```typescript
148
- import { getPrimitiveType } from "@zod-utils/core";
434
+ import { extractDefaultValue } from "@zod-utils/core";
149
435
  import { z } from "zod";
150
436
 
151
- const field = z.string().optional().nullable();
152
- const primitive = getPrimitiveType(field);
153
- // Returns the underlying string schema
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
154
465
 
155
- const arrayField = z.array(z.string()).optional();
156
- const arrayPrimitive = getPrimitiveType(arrayField);
157
- // Returns the ZodArray (stops at arrays)
466
+ ```typescript
467
+ // Extracts input default, not output
468
+ const schema = z
469
+ .string()
470
+ .default("hello")
471
+ .transform((val) => val.toUpperCase());
158
472
 
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)
473
+ extractDefaultValue(schema); // 'hello' (not 'HELLO')
474
+ ```
475
+
476
+ #### With Unions
477
+
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
482
+
483
+ extractDefaultValue(z.union([z.string().default("hello"), z.null()]));
484
+ // 'hello' - union with nullish types extracts from first option
163
485
  ```
164
486
 
165
487
  ---
166
488
 
167
- ### `removeDefault(field)`
489
+ ### `getPrimitiveType(field)`
490
+
491
+ Get the primitive type of a Zod field by unwrapping optional/nullable/default/transform wrappers. Stops at arrays without unwrapping them.
168
492
 
169
- Remove default values from a Zod field.
493
+ #### Basic Usage
170
494
 
171
495
  ```typescript
172
- import { removeDefault } from "@zod-utils/core";
496
+ import { getPrimitiveType } from "@zod-utils/core";
173
497
  import { z } from "zod";
174
498
 
175
- const withDefault = z.string().default("hello");
176
- const withoutDefault = removeDefault(withDefault);
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
+ ```
177
507
 
178
- withDefault.parse(undefined); // 'hello'
179
- withoutDefault.parse(undefined); // throws error
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
180
514
  ```
181
515
 
182
- ---
516
+ #### With Transforms
183
517
 
184
- ### `extractDefaultValue(field)`
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
528
+
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
185
533
 
186
- Extract the default value from a Zod field (recursively unwraps optional/nullable/union/transform layers).
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
537
+ ```
538
+
539
+ ---
187
540
 
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).
541
+ ### `removeDefault(field)`
189
542
 
190
- **Transform support:** Automatically unwraps `.transform()` to get the input type's default value.
543
+ Remove default values from a Zod field while preserving optional/nullable wrappers.
191
544
 
192
545
  ```typescript
193
- import { extractDefaultValue } from "@zod-utils/core";
546
+ import { removeDefault } from "@zod-utils/core";
194
547
  import { z } from "zod";
195
548
 
196
- // Basic usage
197
- const field = z.string().optional().default("hello");
198
- extractDefaultValue(field); // 'hello'
199
-
200
- const noDefault = z.string();
201
- extractDefaultValue(noDefault); // undefined
549
+ // Remove default
550
+ const withDefault = z.string().default("hello");
551
+ const withoutDefault = removeDefault(withDefault);
202
552
 
203
- // Union with default in first option
204
- const unionField = z.union([z.string().default('hello'), z.number()]);
205
- extractDefaultValue(unionField); // 'hello'
553
+ withDefault.parse(undefined); // 'hello'
554
+ withoutDefault.parse(undefined); // throws ZodError
206
555
 
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
556
+ // Preserves optional/nullable
557
+ const complex = z.string().default("test").optional().nullable();
558
+ const removed = removeDefault(complex);
210
559
 
211
- // Union wrapped in optional
212
- const wrappedUnion = z.union([z.string().default('test'), z.number()]).optional();
213
- extractDefaultValue(wrappedUnion); // 'test'
560
+ removed.parse(undefined); // undefined (optional still works)
561
+ removed.parse(null); // null (nullable still works)
562
+ removed.parse("hello"); // 'hello'
214
563
 
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')
564
+ // Returns same schema if no default
565
+ const plain = z.string();
566
+ removeDefault(plain) === plain; // true
218
567
  ```
219
568
 
220
569
  ---
221
570
 
222
571
  ### `getFieldChecks(field)`
223
572
 
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.
573
+ Extract all validation check definitions from a Zod schema field. Returns Zod's raw check definition objects.
225
574
 
226
- **Automatically unwraps:** optional, nullable, and default layers. For unions, checks only the first option.
227
-
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`
575
+ #### String Validations
235
576
 
236
577
  ```typescript
237
578
  import { getFieldChecks } from "@zod-utils/core";
238
579
  import { z } from "zod";
239
580
 
240
- // String with length constraints
241
- const username = z.string().min(3).max(20);
242
- const checks = getFieldChecks(username);
581
+ // Length constraints
582
+ getFieldChecks(z.string().min(3));
583
+ // [{ check: 'min_length', minimum: 3, ... }]
584
+
585
+ getFieldChecks(z.string().max(100));
586
+ // [{ check: 'max_length', maximum: 100, ... }]
587
+
588
+ getFieldChecks(z.string().min(3).max(20));
243
589
  // [
244
- // { check: 'min_length', minimum: 3, when: [Function], ... },
245
- // { check: 'max_length', maximum: 20, when: [Function], ... }
590
+ // { check: 'min_length', minimum: 3, ... },
591
+ // { check: 'max_length', maximum: 20, ... }
246
592
  // ]
247
593
 
248
- // Number with range constraints
249
- const age = z.number().min(18).max(120);
250
- const checks = getFieldChecks(age);
594
+ getFieldChecks(z.string().length(10));
595
+ // [{ check: 'length_equals', length: 10, ... }]
596
+
597
+ // Format validations
598
+ getFieldChecks(z.string().email());
599
+ // [{ check: 'string_format', format: 'email', ... }]
600
+
601
+ getFieldChecks(z.string().url());
602
+ // [{ check: 'string_format', format: 'url', ... }]
603
+
604
+ getFieldChecks(z.string().uuid());
605
+ // [{ check: 'string_format', format: 'uuid', ... }]
606
+ ```
607
+
608
+ #### Number Validations
609
+
610
+ ```typescript
611
+ getFieldChecks(z.number().min(18));
612
+ // [{ check: 'greater_than', value: 18, inclusive: true, ... }]
613
+
614
+ getFieldChecks(z.number().max(120));
615
+ // [{ check: 'less_than', value: 120, inclusive: true, ... }]
616
+
617
+ getFieldChecks(z.number().min(0).max(100));
251
618
  // [
252
- // { check: 'greater_than', value: 18, inclusive: true, ... },
253
- // { check: 'less_than', value: 120, inclusive: true, ... }
619
+ // { check: 'greater_than', value: 0, inclusive: true, ... },
620
+ // { check: 'less_than', value: 100, inclusive: true, ... }
254
621
  // ]
255
622
 
256
- // Array with item count constraints
257
- const tags = z.array(z.string()).min(1).max(5);
258
- const checks = getFieldChecks(tags);
623
+ getFieldChecks(z.number().gt(0)); // exclusive >
624
+ // [{ check: 'greater_than', value: 0, inclusive: false, ... }]
625
+
626
+ getFieldChecks(z.number().lt(100)); // exclusive <
627
+ // [{ check: 'less_than', value: 100, inclusive: false, ... }]
628
+ ```
629
+
630
+ #### Array Validations
631
+
632
+ ```typescript
633
+ getFieldChecks(z.array(z.string()).min(1));
634
+ // [{ check: 'min_length', minimum: 1, ... }]
635
+
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");
651
+
652
+ getFieldChecks(z.date().min(minDate));
653
+ // [{ check: 'greater_than', value: Date, inclusive: true, ... }]
268
654
 
269
- // Unwrapping optional/nullable/default layers
270
- const bio = z.string().min(10).max(500).optional();
271
- const checks = getFieldChecks(bio);
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
276
666
 
277
- // No checks
278
- const plainString = z.string();
279
- getFieldChecks(plainString); // []
667
+ ```typescript
668
+ // Automatically unwraps optional/nullable/default
669
+ getFieldChecks(z.string().min(3).max(20).optional());
670
+ // [{ check: 'min_length', ... }, { check: 'max_length', ... }]
671
+
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', ... }]
682
+ ```
683
+
684
+ #### No Constraints
685
+
686
+ ```typescript
687
+ // Returns empty array when no constraints
688
+ getFieldChecks(z.string()); // []
689
+ getFieldChecks(z.number()); // []
690
+ getFieldChecks(z.boolean()); // []
691
+ getFieldChecks(z.date()); // []
280
692
  ```
281
693
 
282
- **Type:** The return type is `ZodUnionCheck[]`, a union of all 21 Zod check definition types. You can also import the `ZodUnionCheck` type:
694
+ #### Using Checks in UI
283
695
 
284
696
  ```typescript
285
- import { getFieldChecks, type ZodUnionCheck } from "@zod-utils/core";
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,107 +759,166 @@ 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
+ }),
847
+ });
848
+
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
+ ),
353
865
  });
354
- // Returns: ZodString
355
866
 
356
- // Discriminated union schema
357
- const formSchema = z.discriminatedUnion('mode', [
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
387
901
 
388
- // Works with transforms - extracts from input type
389
- const transformedSchema = z
902
+ // Without discriminator, returns undefined
903
+ extractFieldFromSchema({
904
+ schema: formSchema,
905
+ name: "name",
906
+ }); // undefined
907
+ ```
908
+
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)
401
- ```
402
-
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
- ---
406
-
407
- ### `toFieldSelector(props)`
408
-
409
- Extracts a `FieldSelector` from props containing schema, name, and optional discriminator. Encapsulates type assertion so callers don't need eslint-disable.
410
-
411
- ```typescript
412
- import { toFieldSelector } from "@zod-utils/core";
413
-
414
- const selectorProps = toFieldSelector({ schema, name: 'fieldName', discriminator });
415
- // Use selectorProps for extractFieldFromSchema, etc.
919
+ // Extracts from input type (before transform)
920
+ extractFieldFromSchema({ schema, name: "name" }); // ZodString
921
+ extractFieldFromSchema({ schema, name: "age" }); // ZodNumber
416
922
  ```
417
923
 
418
924
  ---
@@ -421,99 +927,74 @@ const selectorProps = toFieldSelector({ schema, name: 'fieldName', discriminator
421
927
 
422
928
  Extends a Zod field with a transformation while preserving its metadata.
423
929
 
424
- This is useful when you want to add validations or transformations to a field but keep the original metadata (like `translationKey`) intact.
425
-
426
930
  ```typescript
427
931
  import { extendWithMeta } from "@zod-utils/core";
428
932
  import { z } from "zod";
429
933
 
430
934
  // Base field with metadata
431
- const baseField = z.string().meta({ translationKey: 'user.field.name' });
935
+ const baseField = z.string().meta({ translationKey: "user.field.name" });
432
936
 
433
937
  // Extend with validation while keeping metadata
434
938
  const extendedField = extendWithMeta(baseField, (f) => f.min(3).max(100));
435
939
 
436
940
  extendedField.meta(); // { translationKey: 'user.field.name' }
437
-
438
- // Validation still works
439
- extendedField.parse('ab'); // throws - too short
440
- extendedField.parse('abc'); // 'abc' - valid
941
+ extendedField.parse("ab"); // throws - too short
942
+ extendedField.parse("abc"); // 'abc' - valid
441
943
  ```
442
944
 
443
- **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
444
946
 
445
947
  ```typescript
446
948
  // Shared field definitions with i18n metadata
447
949
  const fields = {
448
- name: z.string().meta({ translationKey: 'user.field.name' }),
449
- 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" }),
450
953
  };
451
954
 
452
955
  // Create form uses base fields with additional constraints
453
956
  const createFormSchema = z.object({
454
957
  name: extendWithMeta(fields.name, (f) => f.min(3).max(50)),
455
958
  email: extendWithMeta(fields.email, (f) => f.min(5)),
959
+ age: extendWithMeta(fields.age, (f) => f.min(18).max(120)),
456
960
  });
457
961
 
458
962
  // Edit form uses same fields with different constraints
459
963
  const editFormSchema = z.object({
460
964
  name: extendWithMeta(fields.name, (f) => f.optional()),
461
965
  email: fields.email, // no extension needed
966
+ age: extendWithMeta(fields.age, (f) => f.optional()),
462
967
  });
463
968
  ```
464
969
 
465
- **Note:** If the original field has no metadata, the transformed field is returned as-is without calling `.meta()`.
466
-
467
970
  ---
468
971
 
469
- ### `getSchemaDefaults(schema, discriminator?)`
972
+ ### `toFieldSelector(props)`
470
973
 
471
- **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.
472
975
 
473
976
  ```typescript
474
- import { getSchemaDefaults } from "@zod-utils/core";
475
- import { z } from "zod";
977
+ import { toFieldSelector } from "@zod-utils/core";
476
978
 
477
- // Discriminated union with defaults
478
- const formSchema = z.discriminatedUnion('mode', [
479
- z.object({
480
- mode: z.literal('create'),
481
- name: z.string(),
482
- age: z.number().default(18),
483
- }),
484
- z.object({
485
- mode: z.literal('edit'),
486
- id: z.number().default(1),
487
- name: z.string().optional(),
488
- bio: z.string().default('bio goes here'),
489
- }),
490
- ]);
979
+ const schema = z.object({ name: z.string(), age: z.number() });
491
980
 
492
- // Get defaults for 'create' mode
493
- const createDefaults = getSchemaDefaults(formSchema, {
494
- discriminator: {
495
- key: 'mode',
496
- value: 'create',
497
- },
498
- });
499
- // Returns: { age: 18 }
981
+ const selector = toFieldSelector({ schema, name: "name" });
982
+ // { schema, name: 'name' }
500
983
 
501
- // Get defaults for 'edit' mode
502
- const editDefaults = getSchemaDefaults(formSchema, {
503
- discriminator: {
504
- key: 'mode',
505
- value: 'edit',
506
- },
507
- });
508
- // 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
+ ]);
509
989
 
510
- // Without discriminator, returns empty object
511
- const noDefaults = getSchemaDefaults(formSchema);
512
- // 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' } }
513
996
  ```
514
997
 
515
- **Discriminator types:** The discriminator `value` can be a string, number, or boolean literal that matches the discriminator field type.
516
-
517
998
  ---
518
999
 
519
1000
  ## Type Utilities
@@ -532,40 +1013,67 @@ type Simple = Simplify<Complex>;
532
1013
 
533
1014
  ---
534
1015
 
535
- ### `DiscriminatedInput<TSchema, TDiscriminatorKey, TDiscriminatorValue>`
1016
+ ### `Paths<T, FilterType?, Strict?>`
536
1017
 
537
- 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.
1018
+ Generate dot-notation paths from any type. Low-level utility for building path strings.
1019
+
1020
+ #### Basic Usage
538
1021
 
539
1022
  ```typescript
540
- import type { DiscriminatedInput } from "@zod-utils/core";
541
- import { z } from "zod";
1023
+ import type { Paths } from "@zod-utils/core";
542
1024
 
543
- const schema = z.discriminatedUnion("mode", [
544
- z.object({ mode: z.literal("create"), name: z.string() }),
545
- z.object({ mode: z.literal("edit"), id: z.number() }),
546
- ]);
1025
+ type User = {
1026
+ name: string;
1027
+ age: number;
1028
+ profile: {
1029
+ bio: string;
1030
+ avatar: string;
1031
+ };
1032
+ tags: string[];
1033
+ };
547
1034
 
548
- type CreateInput = DiscriminatedInput<typeof schema, "mode", "create">;
549
- // { mode: 'create'; name: string }
1035
+ // All paths
1036
+ type AllPaths = Paths<User>;
1037
+ // "name" | "age" | "profile" | "profile.bio" | "profile.avatar" | "tags" | "tags.0"
1038
+ ```
550
1039
 
551
- type EditInput = DiscriminatedInput<typeof schema, "mode", "edit">;
552
- // { 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"
553
1068
  ```
554
1069
 
555
1070
  ---
556
1071
 
557
1072
  ### `ValidPaths<TSchema, TDiscriminatorKey?, TDiscriminatorValue?, TFilterType?, TStrict?>`
558
1073
 
559
- Generates valid dot-notation paths for a schema, with optional type filtering and discriminated union support.
560
-
561
- **Parameters:**
562
- - `TSchema` - The Zod schema type
563
- - `TDiscriminatorKey` - Discriminator key for discriminated unions (default: `never`)
564
- - `TDiscriminatorValue` - Discriminator value to filter variant (default: `never`)
565
- - `TFilterType` - Type to filter paths by (default: `unknown` = all paths)
566
- - `TStrict` - Strict mode for type matching (default: `true`)
1074
+ Generates valid dot-notation paths for a Zod schema, with optional type filtering and discriminated union support.
567
1075
 
568
- **Basic usage:**
1076
+ #### Basic Usage
569
1077
 
570
1078
  ```typescript
571
1079
  import type { ValidPaths } from "@zod-utils/core";
@@ -578,20 +1086,24 @@ const schema = z.object({
578
1086
  active: z.boolean(),
579
1087
  });
580
1088
 
581
- // Get all paths (no filtering)
1089
+ // All paths
582
1090
  type AllPaths = ValidPaths<typeof schema>;
583
1091
  // "name" | "age" | "email" | "active"
1092
+ ```
584
1093
 
585
- // Filter by type - only string fields
1094
+ #### Filter by Type
1095
+
1096
+ ```typescript
1097
+ // Only string fields
586
1098
  type StringPaths = ValidPaths<typeof schema, never, never, string>;
587
1099
  // "name"
588
1100
 
589
1101
  // Non-strict mode - includes optional string fields
590
- type StringPathsNonStrict = ValidPaths<typeof schema, never, never, string, false>;
1102
+ type LooseStringPaths = ValidPaths<typeof schema, never, never, string, false>;
591
1103
  // "name" | "email"
592
1104
  ```
593
1105
 
594
- **With discriminated unions:**
1106
+ #### With Discriminated Unions
595
1107
 
596
1108
  ```typescript
597
1109
  const formSchema = z.discriminatedUnion("mode", [
@@ -599,55 +1111,300 @@ const formSchema = z.discriminatedUnion("mode", [
599
1111
  z.object({ mode: z.literal("edit"), id: z.number(), title: z.string() }),
600
1112
  ]);
601
1113
 
602
- // Get all paths for 'create' variant
1114
+ // All paths for 'create' variant
603
1115
  type CreatePaths = ValidPaths<typeof formSchema, "mode", "create">;
604
1116
  // "mode" | "name" | "age"
605
1117
 
606
- // 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
607
1123
  type EditNumberPaths = ValidPaths<typeof formSchema, "mode", "edit", number>;
608
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"
1169
+ ```
1170
+
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
609
1340
  ```
610
1341
 
611
- **Strict vs Non-Strict mode:**
1342
+ #### Optional and Nullable Fields with Strict Mode
612
1343
 
613
1344
  ```typescript
614
1345
  const schema = z.object({
615
1346
  required: z.string(),
616
1347
  optional: z.string().optional(),
617
1348
  nullable: z.string().nullable(),
1349
+ optionalNullable: z.string().optional().nullable(),
1350
+ withDefault: z.string().default("hello"),
618
1351
  });
619
1352
 
620
- // Strict mode (default) - exact type matching
621
- type StrictPaths = ValidPaths<typeof schema, never, never, string>;
622
- // "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
623
1357
 
624
- // Non-strict mode - includes subtypes
625
- type NonStrictPaths = ValidPaths<typeof schema, never, never, string, false>;
626
- // "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"
627
1361
  ```
628
1362
 
629
- ---
630
-
631
- ### `Paths<T, FilterType?, Strict?>`
632
-
633
- Low-level type utility for generating dot-notation paths from any type (not schema-specific).
1363
+ #### Complex Discriminated Union with Nested Arrays
634
1364
 
635
1365
  ```typescript
636
- 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
+ ]);
637
1394
 
638
- type User = {
639
- name: string;
640
- age: number;
641
- profile: { bio: string };
642
- };
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"
643
1400
 
644
- // All paths
645
- type AllPaths = Paths<User>;
646
- // "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"
647
1404
 
648
- // Filtered by string
649
- type StringPaths = Paths<User, string>;
650
- // "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"
651
1408
  ```
652
1409
 
653
1410
  ---
@@ -676,76 +1433,60 @@ type FormParams = FieldSelector<typeof formSchema, "name", "mode", "create">;
676
1433
 
677
1434
  ---
678
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
+
679
1458
  ## Migration Guide
680
1459
 
681
1460
  ### Migrating to v3.0.0
682
1461
 
683
1462
  #### `ValidPathsOfType` removed → Use `ValidPaths` with type filtering
684
1463
 
685
- The `ValidPathsOfType` type has been removed and consolidated into `ValidPaths` with a new `TFilterType` parameter.
686
-
687
- **Before (v2.x):**
688
1464
  ```typescript
1465
+ // Before (v2.x)
689
1466
  import type { ValidPathsOfType } from "@zod-utils/core";
690
-
691
- // Get string field paths
692
1467
  type StringPaths = ValidPathsOfType<typeof schema, string>;
693
-
694
- // With discriminated union
695
1468
  type EditNumberPaths = ValidPathsOfType<typeof schema, number, "mode", "edit">;
696
- ```
697
1469
 
698
- **After (v3.x):**
699
- ```typescript
1470
+ // After (v3.x)
700
1471
  import type { ValidPaths } from "@zod-utils/core";
701
-
702
- // Get string field paths - use 4th type parameter
703
1472
  type StringPaths = ValidPaths<typeof schema, never, never, string>;
704
-
705
- // With discriminated union - TFilterType is now 4th parameter
706
1473
  type EditNumberPaths = ValidPaths<typeof schema, "mode", "edit", number>;
707
1474
  ```
708
1475
 
709
- #### `Paths<T>` signature changed
710
-
711
- The `Paths` type now accepts optional `FilterType` and `Strict` parameters.
712
-
713
- **Before (v2.x):**
714
- ```typescript
715
- type AllPaths = Paths<User>; // Still works the same
716
- ```
717
-
718
- **After (v3.x):**
719
- ```typescript
720
- type AllPaths = Paths<User>; // Works the same (backward compatible)
721
-
722
- // New: Filter by type
723
- type StringPaths = Paths<User, string>;
724
-
725
- // New: Non-strict mode
726
- type StringPaths = Paths<User, string, false>;
727
- ```
728
-
729
1476
  ### Migrating to v4.0.0
730
1477
 
731
1478
  #### `mergeFieldSelectorProps` renamed → Use `toFieldSelector`
732
1479
 
733
- The function has been renamed and simplified to accept a single props object.
734
-
735
- **Before (v3.x):**
736
1480
  ```typescript
1481
+ // Before (v3.x)
737
1482
  import { mergeFieldSelectorProps } from "@zod-utils/core";
738
-
739
1483
  const selectorProps = mergeFieldSelectorProps(
740
1484
  { schema },
741
1485
  { name, discriminator }
742
1486
  );
743
- ```
744
1487
 
745
- **After (v4.x):**
746
- ```typescript
1488
+ // After (v4.x)
747
1489
  import { toFieldSelector } from "@zod-utils/core";
748
-
749
1490
  const selectorProps = toFieldSelector({ schema, name, discriminator });
750
1491
  ```
751
1492