@zod-utils/core 0.2.0 → 0.4.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
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@zod-utils/core.svg)](https://www.npmjs.com/package/@zod-utils/core)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@zod-utils/core.svg)](https://www.npmjs.com/package/@zod-utils/core)
5
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@zod-utils/core)](https://bundlephobia.com/package/@zod-utils/core)
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)
7
8
  [![CI](https://github.com/thu-san/zod-utils/workflows/CI/badge.svg)](https://github.com/thu-san/zod-utils/actions)
@@ -15,10 +16,14 @@ Pure TypeScript utilities for Zod schema manipulation and default extraction. No
15
16
  npm install @zod-utils/core zod
16
17
  ```
17
18
 
19
+ ## Related Packages
20
+
21
+ - **[@zod-utils/react-hook-form](https://www.npmjs.com/package/@zod-utils/react-hook-form)** - React Hook Form integration with automatic type transformation. Uses this package internally and re-exports all utilities for convenience.
22
+
18
23
  ## Features
19
24
 
20
25
  - 🎯 **Extract defaults** - Get default values from Zod schemas
21
- - ✅ **Check required fields** - Determine if fields are required
26
+ - ✅ **Check validation requirements** - Determine if fields will error on empty input
22
27
  - 🔧 **Schema utilities** - Unwrap and manipulate schema types
23
28
  - 📦 **Zero dependencies** - Only requires Zod as a peer dependency
24
29
  - 🌐 **Universal** - Works in Node.js, browsers, and any TypeScript project
@@ -67,48 +72,64 @@ const defaults = getSchemaDefaults(schema);
67
72
 
68
73
  ---
69
74
 
70
- ### `checkIfFieldIsRequired(field)`
75
+ ### `requiresValidInput(field)`
76
+
77
+ 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).
71
78
 
72
- Check if a Zod field is required. Returns `false` if the field accepts any of:
79
+ **Key insight:** Defaults are just initial values - they don't prevent validation errors if the user clears the field.
73
80
 
74
- - `undefined` (via `.optional()` or `.default()`)
75
- - `null` (via `.nullable()`)
76
- - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)
77
- - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)
81
+ **Real-world example:**
78
82
 
79
83
  ```typescript
80
- import { checkIfFieldIsRequired } from "@zod-utils/core";
81
- import { z } from "zod";
84
+ // Marital status with default but validation rules
85
+ const maritalStatus = z.string().min(1).default('single');
86
+
87
+ // What happens in the form:
88
+ // 1. Initial: field shows "single" (from default)
89
+ // 2. User deletes the value → empty string
90
+ // 3. User submits form → validation fails (.min(1) rejects empty)
91
+ // 4. requiresValidInput(maritalStatus) → true (show *, show error)
92
+ ```
82
93
 
83
- // Required fields - return true
84
- const requiredString = z.string().min(1);
85
- const nonemptyString = z.string().nonempty();
86
- const requiredArray = z.array(z.string()).min(1);
87
- const nonemptyArray = z.array(z.string()).nonempty();
94
+ **How it works:**
88
95
 
89
- checkIfFieldIsRequired(requiredString); // true
90
- checkIfFieldIsRequired(nonemptyString); // true
91
- checkIfFieldIsRequired(requiredArray); // true
92
- checkIfFieldIsRequired(nonemptyArray); // true
96
+ 1. Removes `.default()` wrappers (defaults ≠ validation rules)
97
+ 2. Tests if underlying schema accepts empty/invalid input:
98
+ - `undefined` (via `.optional()`)
99
+ - `null` (via `.nullable()`)
100
+ - Empty string (plain `z.string()`)
101
+ - Empty array (plain `z.array()`)
102
+ 3. Returns `true` if validation will fail on empty input
103
+
104
+ **Examples:**
105
+
106
+ ```typescript
107
+ import { requiresValidInput } from "@zod-utils/core";
108
+ import { z } from "zod";
93
109
 
94
- // Fields accepting undefined - return false
95
- const optionalField = z.string().optional();
96
- const fieldWithDefault = z.string().default("hello");
110
+ // User name - required, no default
111
+ const userName = z.string().min(1);
112
+ requiresValidInput(userName); // true - will error if empty
97
113
 
98
- checkIfFieldIsRequired(optionalField); // false
99
- checkIfFieldIsRequired(fieldWithDefault); // false
114
+ // Marital status - required WITH default
115
+ const maritalStatus = z.string().min(1).default('single');
116
+ requiresValidInput(maritalStatus); // true - will error if user clears it
100
117
 
101
- // Fields accepting empty values - return false
102
- const emptyStringAllowed = z.string();
103
- const emptyArrayAllowed = z.array(z.string());
118
+ // Age with default - requires valid input
119
+ const age = z.number().default(0);
120
+ requiresValidInput(age); // true - numbers reject empty strings
104
121
 
105
- checkIfFieldIsRequired(emptyStringAllowed); // false
106
- checkIfFieldIsRequired(emptyArrayAllowed); // false
122
+ // Optional bio - doesn't require input
123
+ const bio = z.string().optional();
124
+ requiresValidInput(bio); // false - user can leave empty
107
125
 
108
- // Fields accepting null - return false
109
- const nullableField = z.string().nullable();
126
+ // Notes with default but NO validation
127
+ const notes = z.string().default('N/A');
128
+ requiresValidInput(notes); // false - plain z.string() accepts empty
110
129
 
111
- checkIfFieldIsRequired(nullableField); // false
130
+ // Nullable middle name
131
+ const middleName = z.string().nullable();
132
+ requiresValidInput(middleName); // false - user can leave null
112
133
  ```
113
134
 
114
135
  ---
package/dist/index.d.mts CHANGED
@@ -169,6 +169,78 @@ type Unwrappable = {
169
169
  * @since 0.1.0
170
170
  */
171
171
  declare function canUnwrap(field: z.ZodTypeAny): field is z.ZodTypeAny & Unwrappable;
172
+ /**
173
+ * Unwraps a ZodUnion type and returns the first field and all union options.
174
+ *
175
+ * This function extracts the individual type options from a union type.
176
+ * By default, it filters out `ZodNull` and `ZodUndefined` types, returning only
177
+ * the meaningful type options. You can disable this filtering to get all options.
178
+ *
179
+ * @template T - The Zod type to unwrap
180
+ * @param field - The Zod field (union or single type)
181
+ * @param options - Configuration options
182
+ * @param options.filterNullish - Whether to filter out null and undefined types (default: true)
183
+ * @returns Object with `field` (first option) and `union` (all options array)
184
+ *
185
+ * @example
186
+ * Basic union unwrapping
187
+ * ```typescript
188
+ * const field = z.union([z.string(), z.number()]);
189
+ * const result = unwrapUnion(field);
190
+ * // Result: { field: z.string(), union: [z.string(), z.number()] }
191
+ * ```
192
+ *
193
+ * @example
194
+ * Union with null (filtered by default)
195
+ * ```typescript
196
+ * const field = z.union([z.string(), z.null()]);
197
+ * const result = unwrapUnion(field);
198
+ * // Result: { field: z.string(), union: [z.string()] }
199
+ * ```
200
+ *
201
+ * @example
202
+ * Union with null (keep all options)
203
+ * ```typescript
204
+ * const field = z.union([z.string(), z.null()]);
205
+ * const result = unwrapUnion(field, { filterNullish: false });
206
+ * // Result: { field: z.string(), union: [z.string(), z.null()] }
207
+ * ```
208
+ *
209
+ * @example
210
+ * Non-union type (returns single field)
211
+ * ```typescript
212
+ * const field = z.string();
213
+ * const result = unwrapUnion(field);
214
+ * // Result: { field: z.string(), union: [z.string()] }
215
+ * ```
216
+ *
217
+ * @example
218
+ * Nullable as union
219
+ * ```typescript
220
+ * const field = z.string().nullable(); // This is z.union([z.string(), z.null()])
221
+ * const result = unwrapUnion(field);
222
+ * // Result: { field: z.string(), union: [z.string()] } (null filtered out)
223
+ * ```
224
+ *
225
+ * @example
226
+ * Using the first field for type checking
227
+ * ```typescript
228
+ * const field = z.union([z.string(), z.number()]);
229
+ * const { field: firstField, union } = unwrapUnion(field);
230
+ * if (firstField instanceof z.ZodString) {
231
+ * console.log('First type is string');
232
+ * }
233
+ * ```
234
+ *
235
+ * @see {@link getPrimitiveType} for unwrapping wrapper types
236
+ * @since 0.1.0
237
+ */
238
+ declare function unwrapUnion<T extends z.ZodTypeAny>(field: T, options?: {
239
+ filterNullish?: boolean;
240
+ }): {
241
+ field: z.ZodTypeAny;
242
+ union: z.ZodTypeAny[];
243
+ };
172
244
  /**
173
245
  * Gets the underlying primitive type of a Zod field by recursively unwrapping wrapper types.
174
246
  *
@@ -206,7 +278,7 @@ declare function canUnwrap(field: z.ZodTypeAny): field is z.ZodTypeAny & Unwrapp
206
278
  * @see {@link canUnwrap} for checking if a field can be unwrapped
207
279
  * @since 0.1.0
208
280
  */
209
- declare const getPrimitiveType: <T extends z.ZodTypeAny>(field: T) => z.ZodTypeAny;
281
+ declare const getPrimitiveType: <T extends z.ZodType>(field: T) => z.ZodTypeAny;
210
282
  type StripZodDefault<T> = T extends z.ZodDefault<infer Inner> ? StripZodDefault<Inner> : T extends z.ZodOptional<infer Inner> ? z.ZodOptional<StripZodDefault<Inner>> : T extends z.ZodNullable<infer Inner> ? z.ZodNullable<StripZodDefault<Inner>> : T;
211
283
  /**
212
284
  * Removes default values from a Zod field while preserving other wrapper types.
@@ -243,72 +315,87 @@ type StripZodDefault<T> = T extends z.ZodDefault<infer Inner> ? StripZodDefault<
243
315
  * // Result: z.string().nullable()
244
316
  * ```
245
317
  *
246
- * @see {@link checkIfFieldIsRequired} for usage with requirement checking
318
+ * @see {@link requiresValidInput} for usage with requirement checking
247
319
  * @since 0.1.0
248
320
  */
249
321
  declare function removeDefault<T extends z.ZodType>(field: T): StripZodDefault<T>;
250
322
  /**
251
- * Checks if a Zod field is truly required by testing multiple acceptance criteria.
323
+ * Determines if a field will show validation errors when the user submits empty or invalid input.
324
+ *
325
+ * This is useful for form UIs to indicate which fields require valid user input (e.g., showing
326
+ * asterisks, validation states). The key insight: **defaults are just initial values** - they
327
+ * don't prevent validation errors if the user clears the field.
328
+ *
329
+ * **Real-world example:**
330
+ * ```typescript
331
+ * // Marital status field with default but validation rules
332
+ * const maritalStatus = z.string().min(1).default('single');
252
333
  *
253
- * A field is considered **not required** if it accepts any of the following:
254
- * - `undefined` (via `.optional()` or `.default()`)
255
- * - `null` (via `.nullable()`)
256
- * - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)
257
- * - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)
334
+ * // Initial: field shows "single" (from default)
335
+ * // User deletes the value → field is now empty string
336
+ * // User submits form → validation fails because .min(1) rejects empty strings
337
+ * // requiresValidInput(maritalStatus) true (shows * indicator, validation error)
338
+ * ```
258
339
  *
259
- * **Note:** Fields with `.default()` are considered not required since they'll have a value
260
- * even if the user doesn't provide one.
340
+ * **How it works:**
341
+ * 1. Removes `.default()` wrappers (defaults are initial values, not validation rules)
342
+ * 2. Tests if the underlying schema accepts empty/invalid input:
343
+ * - `undefined` (via `.optional()`)
344
+ * - `null` (via `.nullable()`)
345
+ * - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)
346
+ * - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)
347
+ * 3. Returns `true` if validation will fail, `false` if empty input is accepted
261
348
  *
262
349
  * @template T - The Zod type to check
263
- * @param field - The Zod field to check for required status
264
- * @returns True if the field is required, false otherwise
350
+ * @param field - The Zod field to check
351
+ * @returns True if the field will show validation errors on empty/invalid input, false otherwise
265
352
  *
266
353
  * @example
267
- * Required field
354
+ * User name field - required, no default
268
355
  * ```typescript
269
- * const field = z.string().min(1);
270
- * console.log(checkIfFieldIsRequired(field)); // true
356
+ * const userName = z.string().min(1);
357
+ * requiresValidInput(userName); // true - will error if user submits empty
271
358
  * ```
272
359
  *
273
360
  * @example
274
- * Optional field (not required)
361
+ * Marital status - required WITH default
275
362
  * ```typescript
276
- * const field = z.string().optional();
277
- * console.log(checkIfFieldIsRequired(field)); // false
363
+ * const maritalStatus = z.string().min(1).default('single');
364
+ * requiresValidInput(maritalStatus); // true - will error if user clears and submits
278
365
  * ```
279
366
  *
280
367
  * @example
281
- * Field with default (not required)
368
+ * Age with default - requires valid input
282
369
  * ```typescript
283
- * const field = z.string().default('hello');
284
- * console.log(checkIfFieldIsRequired(field)); // false
370
+ * const age = z.number().default(0);
371
+ * requiresValidInput(age); // true - numbers reject empty strings
285
372
  * ```
286
373
  *
287
374
  * @example
288
- * String without min length (not required - accepts empty string)
375
+ * Optional bio field - doesn't require input
289
376
  * ```typescript
290
- * const field = z.string();
291
- * console.log(checkIfFieldIsRequired(field)); // false
377
+ * const bio = z.string().optional();
378
+ * requiresValidInput(bio); // false - user can leave empty
292
379
  * ```
293
380
  *
294
381
  * @example
295
- * String with nonempty (required)
382
+ * String with default but NO validation - doesn't require input
296
383
  * ```typescript
297
- * const field = z.string().nonempty();
298
- * console.log(checkIfFieldIsRequired(field)); // true
384
+ * const notes = z.string().default('N/A');
385
+ * requiresValidInput(notes); // false - plain z.string() accepts empty strings
299
386
  * ```
300
387
  *
301
388
  * @example
302
- * Nullable field (not required)
389
+ * Nullable field - doesn't require input
303
390
  * ```typescript
304
- * const field = z.number().nullable();
305
- * console.log(checkIfFieldIsRequired(field)); // false
391
+ * const middleName = z.string().nullable();
392
+ * requiresValidInput(middleName); // false - user can leave null
306
393
  * ```
307
394
  *
308
395
  * @see {@link removeDefault} for understanding how defaults are handled
309
396
  * @see {@link getPrimitiveType} for understanding type unwrapping
310
397
  * @since 0.1.0
311
398
  */
312
- declare const checkIfFieldIsRequired: <T extends z.ZodType>(field: T) => boolean;
399
+ declare const requiresValidInput: <T extends z.ZodType>(field: T) => boolean;
313
400
 
314
- export { type Simplify, canUnwrap, checkIfFieldIsRequired, extractDefault, getPrimitiveType, getSchemaDefaults, removeDefault };
401
+ export { type Simplify, canUnwrap, extractDefault, getPrimitiveType, getSchemaDefaults, removeDefault, requiresValidInput, unwrapUnion };
package/dist/index.d.ts CHANGED
@@ -169,6 +169,78 @@ type Unwrappable = {
169
169
  * @since 0.1.0
170
170
  */
171
171
  declare function canUnwrap(field: z.ZodTypeAny): field is z.ZodTypeAny & Unwrappable;
172
+ /**
173
+ * Unwraps a ZodUnion type and returns the first field and all union options.
174
+ *
175
+ * This function extracts the individual type options from a union type.
176
+ * By default, it filters out `ZodNull` and `ZodUndefined` types, returning only
177
+ * the meaningful type options. You can disable this filtering to get all options.
178
+ *
179
+ * @template T - The Zod type to unwrap
180
+ * @param field - The Zod field (union or single type)
181
+ * @param options - Configuration options
182
+ * @param options.filterNullish - Whether to filter out null and undefined types (default: true)
183
+ * @returns Object with `field` (first option) and `union` (all options array)
184
+ *
185
+ * @example
186
+ * Basic union unwrapping
187
+ * ```typescript
188
+ * const field = z.union([z.string(), z.number()]);
189
+ * const result = unwrapUnion(field);
190
+ * // Result: { field: z.string(), union: [z.string(), z.number()] }
191
+ * ```
192
+ *
193
+ * @example
194
+ * Union with null (filtered by default)
195
+ * ```typescript
196
+ * const field = z.union([z.string(), z.null()]);
197
+ * const result = unwrapUnion(field);
198
+ * // Result: { field: z.string(), union: [z.string()] }
199
+ * ```
200
+ *
201
+ * @example
202
+ * Union with null (keep all options)
203
+ * ```typescript
204
+ * const field = z.union([z.string(), z.null()]);
205
+ * const result = unwrapUnion(field, { filterNullish: false });
206
+ * // Result: { field: z.string(), union: [z.string(), z.null()] }
207
+ * ```
208
+ *
209
+ * @example
210
+ * Non-union type (returns single field)
211
+ * ```typescript
212
+ * const field = z.string();
213
+ * const result = unwrapUnion(field);
214
+ * // Result: { field: z.string(), union: [z.string()] }
215
+ * ```
216
+ *
217
+ * @example
218
+ * Nullable as union
219
+ * ```typescript
220
+ * const field = z.string().nullable(); // This is z.union([z.string(), z.null()])
221
+ * const result = unwrapUnion(field);
222
+ * // Result: { field: z.string(), union: [z.string()] } (null filtered out)
223
+ * ```
224
+ *
225
+ * @example
226
+ * Using the first field for type checking
227
+ * ```typescript
228
+ * const field = z.union([z.string(), z.number()]);
229
+ * const { field: firstField, union } = unwrapUnion(field);
230
+ * if (firstField instanceof z.ZodString) {
231
+ * console.log('First type is string');
232
+ * }
233
+ * ```
234
+ *
235
+ * @see {@link getPrimitiveType} for unwrapping wrapper types
236
+ * @since 0.1.0
237
+ */
238
+ declare function unwrapUnion<T extends z.ZodTypeAny>(field: T, options?: {
239
+ filterNullish?: boolean;
240
+ }): {
241
+ field: z.ZodTypeAny;
242
+ union: z.ZodTypeAny[];
243
+ };
172
244
  /**
173
245
  * Gets the underlying primitive type of a Zod field by recursively unwrapping wrapper types.
174
246
  *
@@ -206,7 +278,7 @@ declare function canUnwrap(field: z.ZodTypeAny): field is z.ZodTypeAny & Unwrapp
206
278
  * @see {@link canUnwrap} for checking if a field can be unwrapped
207
279
  * @since 0.1.0
208
280
  */
209
- declare const getPrimitiveType: <T extends z.ZodTypeAny>(field: T) => z.ZodTypeAny;
281
+ declare const getPrimitiveType: <T extends z.ZodType>(field: T) => z.ZodTypeAny;
210
282
  type StripZodDefault<T> = T extends z.ZodDefault<infer Inner> ? StripZodDefault<Inner> : T extends z.ZodOptional<infer Inner> ? z.ZodOptional<StripZodDefault<Inner>> : T extends z.ZodNullable<infer Inner> ? z.ZodNullable<StripZodDefault<Inner>> : T;
211
283
  /**
212
284
  * Removes default values from a Zod field while preserving other wrapper types.
@@ -243,72 +315,87 @@ type StripZodDefault<T> = T extends z.ZodDefault<infer Inner> ? StripZodDefault<
243
315
  * // Result: z.string().nullable()
244
316
  * ```
245
317
  *
246
- * @see {@link checkIfFieldIsRequired} for usage with requirement checking
318
+ * @see {@link requiresValidInput} for usage with requirement checking
247
319
  * @since 0.1.0
248
320
  */
249
321
  declare function removeDefault<T extends z.ZodType>(field: T): StripZodDefault<T>;
250
322
  /**
251
- * Checks if a Zod field is truly required by testing multiple acceptance criteria.
323
+ * Determines if a field will show validation errors when the user submits empty or invalid input.
324
+ *
325
+ * This is useful for form UIs to indicate which fields require valid user input (e.g., showing
326
+ * asterisks, validation states). The key insight: **defaults are just initial values** - they
327
+ * don't prevent validation errors if the user clears the field.
328
+ *
329
+ * **Real-world example:**
330
+ * ```typescript
331
+ * // Marital status field with default but validation rules
332
+ * const maritalStatus = z.string().min(1).default('single');
252
333
  *
253
- * A field is considered **not required** if it accepts any of the following:
254
- * - `undefined` (via `.optional()` or `.default()`)
255
- * - `null` (via `.nullable()`)
256
- * - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)
257
- * - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)
334
+ * // Initial: field shows "single" (from default)
335
+ * // User deletes the value → field is now empty string
336
+ * // User submits form → validation fails because .min(1) rejects empty strings
337
+ * // requiresValidInput(maritalStatus) true (shows * indicator, validation error)
338
+ * ```
258
339
  *
259
- * **Note:** Fields with `.default()` are considered not required since they'll have a value
260
- * even if the user doesn't provide one.
340
+ * **How it works:**
341
+ * 1. Removes `.default()` wrappers (defaults are initial values, not validation rules)
342
+ * 2. Tests if the underlying schema accepts empty/invalid input:
343
+ * - `undefined` (via `.optional()`)
344
+ * - `null` (via `.nullable()`)
345
+ * - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)
346
+ * - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)
347
+ * 3. Returns `true` if validation will fail, `false` if empty input is accepted
261
348
  *
262
349
  * @template T - The Zod type to check
263
- * @param field - The Zod field to check for required status
264
- * @returns True if the field is required, false otherwise
350
+ * @param field - The Zod field to check
351
+ * @returns True if the field will show validation errors on empty/invalid input, false otherwise
265
352
  *
266
353
  * @example
267
- * Required field
354
+ * User name field - required, no default
268
355
  * ```typescript
269
- * const field = z.string().min(1);
270
- * console.log(checkIfFieldIsRequired(field)); // true
356
+ * const userName = z.string().min(1);
357
+ * requiresValidInput(userName); // true - will error if user submits empty
271
358
  * ```
272
359
  *
273
360
  * @example
274
- * Optional field (not required)
361
+ * Marital status - required WITH default
275
362
  * ```typescript
276
- * const field = z.string().optional();
277
- * console.log(checkIfFieldIsRequired(field)); // false
363
+ * const maritalStatus = z.string().min(1).default('single');
364
+ * requiresValidInput(maritalStatus); // true - will error if user clears and submits
278
365
  * ```
279
366
  *
280
367
  * @example
281
- * Field with default (not required)
368
+ * Age with default - requires valid input
282
369
  * ```typescript
283
- * const field = z.string().default('hello');
284
- * console.log(checkIfFieldIsRequired(field)); // false
370
+ * const age = z.number().default(0);
371
+ * requiresValidInput(age); // true - numbers reject empty strings
285
372
  * ```
286
373
  *
287
374
  * @example
288
- * String without min length (not required - accepts empty string)
375
+ * Optional bio field - doesn't require input
289
376
  * ```typescript
290
- * const field = z.string();
291
- * console.log(checkIfFieldIsRequired(field)); // false
377
+ * const bio = z.string().optional();
378
+ * requiresValidInput(bio); // false - user can leave empty
292
379
  * ```
293
380
  *
294
381
  * @example
295
- * String with nonempty (required)
382
+ * String with default but NO validation - doesn't require input
296
383
  * ```typescript
297
- * const field = z.string().nonempty();
298
- * console.log(checkIfFieldIsRequired(field)); // true
384
+ * const notes = z.string().default('N/A');
385
+ * requiresValidInput(notes); // false - plain z.string() accepts empty strings
299
386
  * ```
300
387
  *
301
388
  * @example
302
- * Nullable field (not required)
389
+ * Nullable field - doesn't require input
303
390
  * ```typescript
304
- * const field = z.number().nullable();
305
- * console.log(checkIfFieldIsRequired(field)); // false
391
+ * const middleName = z.string().nullable();
392
+ * requiresValidInput(middleName); // false - user can leave null
306
393
  * ```
307
394
  *
308
395
  * @see {@link removeDefault} for understanding how defaults are handled
309
396
  * @see {@link getPrimitiveType} for understanding type unwrapping
310
397
  * @since 0.1.0
311
398
  */
312
- declare const checkIfFieldIsRequired: <T extends z.ZodType>(field: T) => boolean;
399
+ declare const requiresValidInput: <T extends z.ZodType>(field: T) => boolean;
313
400
 
314
- export { type Simplify, canUnwrap, checkIfFieldIsRequired, extractDefault, getPrimitiveType, getSchemaDefaults, removeDefault };
401
+ export { type Simplify, canUnwrap, extractDefault, getPrimitiveType, getSchemaDefaults, removeDefault, requiresValidInput, unwrapUnion };
package/dist/index.js CHANGED
@@ -26,6 +26,23 @@ var z__namespace = /*#__PURE__*/_interopNamespace(z);
26
26
  function canUnwrap(field) {
27
27
  return "unwrap" in field && typeof field.unwrap === "function";
28
28
  }
29
+ function unwrapUnion(field, options = {}) {
30
+ const { filterNullish = true } = options;
31
+ if (field instanceof z__namespace.ZodUnion) {
32
+ const unionOptions = [...field.def.options];
33
+ const filteredOptions = filterNullish ? unionOptions.filter(
34
+ (option) => !(option instanceof z__namespace.ZodNull) && !(option instanceof z__namespace.ZodUndefined)
35
+ ) : unionOptions;
36
+ return {
37
+ field: filteredOptions[0] || field,
38
+ union: filteredOptions
39
+ };
40
+ }
41
+ return {
42
+ field,
43
+ union: [field]
44
+ };
45
+ }
29
46
  var getPrimitiveType = (field) => {
30
47
  if (field instanceof z__namespace.ZodArray) {
31
48
  return field;
@@ -33,6 +50,9 @@ var getPrimitiveType = (field) => {
33
50
  if (canUnwrap(field)) {
34
51
  return getPrimitiveType(field.unwrap());
35
52
  }
53
+ if (field instanceof z__namespace.ZodUnion) {
54
+ return getPrimitiveType(unwrapUnion(field).field);
55
+ }
36
56
  return field;
37
57
  };
38
58
  function removeDefault(field) {
@@ -50,15 +70,12 @@ function removeDefault(field) {
50
70
  }
51
71
  return field;
52
72
  }
53
- var checkIfFieldIsRequired = (field) => {
54
- const undefinedResult = field.safeParse(void 0).success;
55
- if (undefinedResult) {
56
- return false;
57
- }
73
+ var requiresValidInput = (field) => {
58
74
  const defaultRemovedField = removeDefault(field);
59
75
  if (!(defaultRemovedField instanceof z__namespace.ZodType)) {
60
76
  return false;
61
77
  }
78
+ const undefinedResult = defaultRemovedField.safeParse(void 0).success;
62
79
  const nullResult = defaultRemovedField.safeParse(null).success;
63
80
  const primitiveType = getPrimitiveType(defaultRemovedField);
64
81
  const emptyStringResult = primitiveType.type === "string" && defaultRemovedField.safeParse("").success;
@@ -90,10 +107,11 @@ function getSchemaDefaults(schema) {
90
107
  }
91
108
 
92
109
  exports.canUnwrap = canUnwrap;
93
- exports.checkIfFieldIsRequired = checkIfFieldIsRequired;
94
110
  exports.extractDefault = extractDefault;
95
111
  exports.getPrimitiveType = getPrimitiveType;
96
112
  exports.getSchemaDefaults = getSchemaDefaults;
97
113
  exports.removeDefault = removeDefault;
114
+ exports.requiresValidInput = requiresValidInput;
115
+ exports.unwrapUnion = unwrapUnion;
98
116
  //# sourceMappingURL=index.js.map
99
117
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/schema.ts","../src/defaults.ts"],"names":["z","z2"],"mappings":"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","file":"index.js","sourcesContent":["import * as z from 'zod';\n\n/**\n * Type representing a Zod type that has an unwrap method\n */\ntype Unwrappable = { unwrap: () => z.ZodTypeAny };\n\n/**\n * Type guard to check if a Zod field can be unwrapped (has wrapper types like optional, nullable, default).\n *\n * This checks whether a Zod type has an `unwrap()` method, which is present on wrapper types\n * like `ZodOptional`, `ZodNullable`, `ZodDefault`, and others.\n *\n * @param field - The Zod field to check\n * @returns True if the field has an unwrap method, false otherwise\n *\n * @example\n * ```typescript\n * const optionalField = z.string().optional();\n * console.log(canUnwrap(optionalField)); // true\n *\n * const plainField = z.string();\n * console.log(canUnwrap(plainField)); // false\n * ```\n *\n * @since 0.1.0\n */\nexport function canUnwrap(\n field: z.ZodTypeAny,\n): field is z.ZodTypeAny & Unwrappable {\n return 'unwrap' in field && typeof field.unwrap === 'function';\n}\n\n/**\n * Gets the underlying primitive type of a Zod field by recursively unwrapping wrapper types.\n *\n * This function removes wrapper layers (optional, nullable, default) to reveal the base type.\n * **Important:** It stops at array types without unwrapping them, treating arrays as primitives.\n *\n * @template T - The Zod type to unwrap\n * @param field - The Zod field to unwrap\n * @returns The unwrapped primitive Zod type\n *\n * @example\n * Unwrapping to string primitive\n * ```typescript\n * const field = z.string().optional().nullable();\n * const primitive = getPrimitiveType(field);\n * // Result: z.string() (unwrapped all wrappers)\n * ```\n *\n * @example\n * Stopping at array type\n * ```typescript\n * const field = z.array(z.string()).optional();\n * const primitive = getPrimitiveType(field);\n * // Result: z.array(z.string()) (stops at array, doesn't unwrap it)\n * ```\n *\n * @example\n * Unwrapping defaults\n * ```typescript\n * const field = z.number().default(0).optional();\n * const primitive = getPrimitiveType(field);\n * // Result: z.number()\n * ```\n *\n * @see {@link canUnwrap} for checking if a field can be unwrapped\n * @since 0.1.0\n */\nexport const getPrimitiveType = <T extends z.ZodTypeAny>(\n field: T,\n): z.ZodTypeAny => {\n // Stop at arrays - don't unwrap them\n if (field instanceof z.ZodArray) {\n return field;\n }\n\n if (canUnwrap(field)) {\n return getPrimitiveType(field.unwrap());\n }\n\n return field;\n};\n\ntype StripZodDefault<T> = T extends z.ZodDefault<infer Inner>\n ? StripZodDefault<Inner>\n : T extends z.ZodOptional<infer Inner>\n ? z.ZodOptional<StripZodDefault<Inner>>\n : T extends z.ZodNullable<infer Inner>\n ? z.ZodNullable<StripZodDefault<Inner>>\n : T;\n\n/**\n * Removes default values from a Zod field while preserving other wrapper types.\n *\n * This function recursively removes `ZodDefault` wrappers from a field, while maintaining\n * `optional()` and `nullable()` wrappers. Useful for scenarios where you want to check\n * field requirements without considering default values.\n *\n * @template T - The Zod type to process\n * @param field - The Zod field to remove defaults from\n * @returns The field without defaults but with optional/nullable preserved\n *\n * @example\n * Removing simple default\n * ```typescript\n * const field = z.string().default('hello');\n * const withoutDefault = removeDefault(field);\n * // Result: z.string()\n * ```\n *\n * @example\n * Preserving optional wrapper\n * ```typescript\n * const field = z.string().default('hello').optional();\n * const withoutDefault = removeDefault(field);\n * // Result: z.string().optional()\n * ```\n *\n * @example\n * Nested defaults\n * ```typescript\n * const field = z.string().default('inner').nullable().default('outer');\n * const withoutDefault = removeDefault(field);\n * // Result: z.string().nullable()\n * ```\n *\n * @see {@link checkIfFieldIsRequired} for usage with requirement checking\n * @since 0.1.0\n */\nexport function removeDefault<T extends z.ZodType>(\n field: T,\n): StripZodDefault<T> {\n if (field instanceof z.ZodDefault) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field.unwrap() as StripZodDefault<T>;\n }\n\n if ('innerType' in field.def && field.def.innerType instanceof z.ZodType) {\n const inner = removeDefault(field.def.innerType);\n // Reconstruct the wrapper with the modified inner type\n if (field instanceof z.ZodOptional) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return inner.optional() as unknown as StripZodDefault<T>;\n }\n if (field instanceof z.ZodNullable) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return inner.nullable() as unknown as StripZodDefault<T>;\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field as StripZodDefault<T>;\n}\n\n/**\n * Checks if a Zod field is truly required by testing multiple acceptance criteria.\n *\n * A field is considered **not required** if it accepts any of the following:\n * - `undefined` (via `.optional()` or `.default()`)\n * - `null` (via `.nullable()`)\n * - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)\n * - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)\n *\n * **Note:** Fields with `.default()` are considered not required since they'll have a value\n * even if the user doesn't provide one.\n *\n * @template T - The Zod type to check\n * @param field - The Zod field to check for required status\n * @returns True if the field is required, false otherwise\n *\n * @example\n * Required field\n * ```typescript\n * const field = z.string().min(1);\n * console.log(checkIfFieldIsRequired(field)); // true\n * ```\n *\n * @example\n * Optional field (not required)\n * ```typescript\n * const field = z.string().optional();\n * console.log(checkIfFieldIsRequired(field)); // false\n * ```\n *\n * @example\n * Field with default (not required)\n * ```typescript\n * const field = z.string().default('hello');\n * console.log(checkIfFieldIsRequired(field)); // false\n * ```\n *\n * @example\n * String without min length (not required - accepts empty string)\n * ```typescript\n * const field = z.string();\n * console.log(checkIfFieldIsRequired(field)); // false\n * ```\n *\n * @example\n * String with nonempty (required)\n * ```typescript\n * const field = z.string().nonempty();\n * console.log(checkIfFieldIsRequired(field)); // true\n * ```\n *\n * @example\n * Nullable field (not required)\n * ```typescript\n * const field = z.number().nullable();\n * console.log(checkIfFieldIsRequired(field)); // false\n * ```\n *\n * @see {@link removeDefault} for understanding how defaults are handled\n * @see {@link getPrimitiveType} for understanding type unwrapping\n * @since 0.1.0\n */\nexport const checkIfFieldIsRequired = <T extends z.ZodType>(field: T) => {\n // First check the original field for undefined - this catches fields with defaults\n const undefinedResult = field.safeParse(undefined).success;\n if (undefinedResult) {\n return false;\n }\n\n const defaultRemovedField = removeDefault(field);\n\n if (!(defaultRemovedField instanceof z.ZodType)) {\n return false;\n }\n\n // Check if field accepts null (nullable)\n const nullResult = defaultRemovedField.safeParse(null).success;\n\n const primitiveType = getPrimitiveType(defaultRemovedField);\n\n const emptyStringResult =\n primitiveType.type === 'string' &&\n defaultRemovedField.safeParse('').success;\n\n const emptyArrayResult =\n primitiveType.type === 'array' && defaultRemovedField.safeParse([]).success;\n\n return (\n !undefinedResult && !nullResult && !emptyStringResult && !emptyArrayResult\n );\n};\n","import * as z from 'zod';\nimport { canUnwrap } from './schema';\nimport type { Simplify } from './types';\n\n/**\n * Extracts the default value from a Zod field, recursively unwrapping optional and nullable layers.\n *\n * This function traverses through wrapper types (like `ZodOptional`, `ZodNullable`) to find\n * the underlying `ZodDefault` and returns its default value. If no default is found, returns `undefined`.\n *\n * @template T - The Zod type to extract default from\n * @param field - The Zod field to extract default from\n * @returns The default value if present, undefined otherwise\n *\n * @example\n * Basic usage with default value\n * ```typescript\n * const field = z.string().default('hello');\n * const defaultValue = extractDefault(field);\n * // Result: 'hello'\n * ```\n *\n * @example\n * Unwrapping optional/nullable layers\n * ```typescript\n * const field = z.string().default('world').optional();\n * const defaultValue = extractDefault(field);\n * // Result: 'world' (unwraps optional to find default)\n * ```\n *\n * @example\n * Field without default\n * ```typescript\n * const field = z.string().optional();\n * const defaultValue = extractDefault(field);\n * // Result: undefined\n * ```\n *\n * @see {@link getSchemaDefaults} for extracting defaults from entire schemas\n * @since 0.1.0\n */\nexport function extractDefault<T extends z.ZodTypeAny>(\n field: T,\n): z.infer<T> | undefined {\n if (field instanceof z.ZodDefault) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field.def.defaultValue as z.infer<T>;\n }\n\n if (canUnwrap(field)) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return extractDefault(field.unwrap()) as z.infer<T>;\n }\n\n return undefined;\n}\n\n/**\n * Extracts default values from a Zod object schema while skipping fields without defaults.\n *\n * This function recursively traverses the schema and collects all fields that have\n * explicit default values defined. Fields without defaults are excluded from the result.\n *\n * **Important:** Nested defaults are NOT extracted unless the parent object also has\n * an explicit `.default()`. This is by design to match Zod's default value behavior.\n *\n * @template T - The Zod object schema type\n * @param schema - The Zod object schema to extract defaults from\n * @returns A partial object containing only fields with default values\n *\n * @example\n * Basic usage\n * ```typescript\n * const schema = z.object({\n * name: z.string().default('John'),\n * age: z.number(), // no default - will be skipped\n * email: z.string().email().optional(),\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { name: 'John' }\n * ```\n *\n * @example\n * Nested objects with defaults\n * ```typescript\n * const schema = z.object({\n * user: z.object({\n * name: z.string().default('Guest')\n * }).default({ name: 'Guest' }), // ✅ Extracted because parent has .default()\n *\n * settings: z.object({\n * theme: z.string().default('light')\n * }), // ❌ NOT extracted - parent has no .default()\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { user: { name: 'Guest' } }\n * ```\n *\n * @example\n * Unwrapping optional/nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string().default('Untitled').optional(),\n * count: z.number().default(0).nullable(),\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { title: 'Untitled', count: 0 }\n * ```\n *\n * @see {@link extractDefault} for extracting defaults from individual fields\n * @since 0.1.0\n */\nexport function getSchemaDefaults<T extends z.ZodObject>(\n schema: T,\n): Simplify<Partial<z.infer<T>>> {\n const defaults: Record<string, unknown> = {};\n\n for (const key in schema.shape) {\n const field = schema.shape[key];\n if (!field) continue;\n\n // Check if this field has an explicit default value\n const defaultValue = extractDefault(field);\n if (defaultValue !== undefined) {\n defaults[key] = defaultValue;\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return defaults as Partial<z.infer<T>>;\n}\n"]}
1
+ {"version":3,"sources":["../src/schema.ts","../src/defaults.ts"],"names":["z","z2"],"mappings":"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","file":"index.js","sourcesContent":["import * as z from 'zod';\n\n/**\n * Type representing a Zod type that has an unwrap method\n */\ntype Unwrappable = { unwrap: () => z.ZodTypeAny };\n\n/**\n * Type guard to check if a Zod field can be unwrapped (has wrapper types like optional, nullable, default).\n *\n * This checks whether a Zod type has an `unwrap()` method, which is present on wrapper types\n * like `ZodOptional`, `ZodNullable`, `ZodDefault`, and others.\n *\n * @param field - The Zod field to check\n * @returns True if the field has an unwrap method, false otherwise\n *\n * @example\n * ```typescript\n * const optionalField = z.string().optional();\n * console.log(canUnwrap(optionalField)); // true\n *\n * const plainField = z.string();\n * console.log(canUnwrap(plainField)); // false\n * ```\n *\n * @since 0.1.0\n */\nexport function canUnwrap(\n field: z.ZodTypeAny,\n): field is z.ZodTypeAny & Unwrappable {\n return 'unwrap' in field && typeof field.unwrap === 'function';\n}\n\n/**\n * Unwraps a ZodUnion type and returns the first field and all union options.\n *\n * This function extracts the individual type options from a union type.\n * By default, it filters out `ZodNull` and `ZodUndefined` types, returning only\n * the meaningful type options. You can disable this filtering to get all options.\n *\n * @template T - The Zod type to unwrap\n * @param field - The Zod field (union or single type)\n * @param options - Configuration options\n * @param options.filterNullish - Whether to filter out null and undefined types (default: true)\n * @returns Object with `field` (first option) and `union` (all options array)\n *\n * @example\n * Basic union unwrapping\n * ```typescript\n * const field = z.union([z.string(), z.number()]);\n * const result = unwrapUnion(field);\n * // Result: { field: z.string(), union: [z.string(), z.number()] }\n * ```\n *\n * @example\n * Union with null (filtered by default)\n * ```typescript\n * const field = z.union([z.string(), z.null()]);\n * const result = unwrapUnion(field);\n * // Result: { field: z.string(), union: [z.string()] }\n * ```\n *\n * @example\n * Union with null (keep all options)\n * ```typescript\n * const field = z.union([z.string(), z.null()]);\n * const result = unwrapUnion(field, { filterNullish: false });\n * // Result: { field: z.string(), union: [z.string(), z.null()] }\n * ```\n *\n * @example\n * Non-union type (returns single field)\n * ```typescript\n * const field = z.string();\n * const result = unwrapUnion(field);\n * // Result: { field: z.string(), union: [z.string()] }\n * ```\n *\n * @example\n * Nullable as union\n * ```typescript\n * const field = z.string().nullable(); // This is z.union([z.string(), z.null()])\n * const result = unwrapUnion(field);\n * // Result: { field: z.string(), union: [z.string()] } (null filtered out)\n * ```\n *\n * @example\n * Using the first field for type checking\n * ```typescript\n * const field = z.union([z.string(), z.number()]);\n * const { field: firstField, union } = unwrapUnion(field);\n * if (firstField instanceof z.ZodString) {\n * console.log('First type is string');\n * }\n * ```\n *\n * @see {@link getPrimitiveType} for unwrapping wrapper types\n * @since 0.1.0\n */\nexport function unwrapUnion<T extends z.ZodTypeAny>(\n field: T,\n options: { filterNullish?: boolean } = {},\n): { field: z.ZodTypeAny; union: z.ZodTypeAny[] } {\n const { filterNullish = true } = options;\n\n if (field instanceof z.ZodUnion) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const unionOptions = [...field.def.options] as z.ZodTypeAny[];\n\n const filteredOptions = filterNullish\n ? unionOptions.filter(\n (option) =>\n !(option instanceof z.ZodNull) &&\n !(option instanceof z.ZodUndefined),\n )\n : unionOptions;\n\n return {\n field: filteredOptions[0] || field,\n union: filteredOptions,\n };\n }\n\n // If it's not a union, return the field itself\n return {\n field,\n union: [field],\n };\n}\n\n/**\n * Gets the underlying primitive type of a Zod field by recursively unwrapping wrapper types.\n *\n * This function removes wrapper layers (optional, nullable, default) to reveal the base type.\n * **Important:** It stops at array types without unwrapping them, treating arrays as primitives.\n *\n * @template T - The Zod type to unwrap\n * @param field - The Zod field to unwrap\n * @returns The unwrapped primitive Zod type\n *\n * @example\n * Unwrapping to string primitive\n * ```typescript\n * const field = z.string().optional().nullable();\n * const primitive = getPrimitiveType(field);\n * // Result: z.string() (unwrapped all wrappers)\n * ```\n *\n * @example\n * Stopping at array type\n * ```typescript\n * const field = z.array(z.string()).optional();\n * const primitive = getPrimitiveType(field);\n * // Result: z.array(z.string()) (stops at array, doesn't unwrap it)\n * ```\n *\n * @example\n * Unwrapping defaults\n * ```typescript\n * const field = z.number().default(0).optional();\n * const primitive = getPrimitiveType(field);\n * // Result: z.number()\n * ```\n *\n * @see {@link canUnwrap} for checking if a field can be unwrapped\n * @since 0.1.0\n */\nexport const getPrimitiveType = <T extends z.ZodType>(\n field: T,\n): z.ZodTypeAny => {\n // Stop at arrays - don't unwrap them\n if (field instanceof z.ZodArray) {\n return field;\n }\n\n if (canUnwrap(field)) {\n return getPrimitiveType(field.unwrap());\n }\n\n if (field instanceof z.ZodUnion) {\n return getPrimitiveType(unwrapUnion(field).field);\n }\n\n return field;\n};\n\ntype StripZodDefault<T> = T extends z.ZodDefault<infer Inner>\n ? StripZodDefault<Inner>\n : T extends z.ZodOptional<infer Inner>\n ? z.ZodOptional<StripZodDefault<Inner>>\n : T extends z.ZodNullable<infer Inner>\n ? z.ZodNullable<StripZodDefault<Inner>>\n : T;\n\n/**\n * Removes default values from a Zod field while preserving other wrapper types.\n *\n * This function recursively removes `ZodDefault` wrappers from a field, while maintaining\n * `optional()` and `nullable()` wrappers. Useful for scenarios where you want to check\n * field requirements without considering default values.\n *\n * @template T - The Zod type to process\n * @param field - The Zod field to remove defaults from\n * @returns The field without defaults but with optional/nullable preserved\n *\n * @example\n * Removing simple default\n * ```typescript\n * const field = z.string().default('hello');\n * const withoutDefault = removeDefault(field);\n * // Result: z.string()\n * ```\n *\n * @example\n * Preserving optional wrapper\n * ```typescript\n * const field = z.string().default('hello').optional();\n * const withoutDefault = removeDefault(field);\n * // Result: z.string().optional()\n * ```\n *\n * @example\n * Nested defaults\n * ```typescript\n * const field = z.string().default('inner').nullable().default('outer');\n * const withoutDefault = removeDefault(field);\n * // Result: z.string().nullable()\n * ```\n *\n * @see {@link requiresValidInput} for usage with requirement checking\n * @since 0.1.0\n */\nexport function removeDefault<T extends z.ZodType>(\n field: T,\n): StripZodDefault<T> {\n if (field instanceof z.ZodDefault) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field.unwrap() as StripZodDefault<T>;\n }\n\n if ('innerType' in field.def && field.def.innerType instanceof z.ZodType) {\n const inner = removeDefault(field.def.innerType);\n // Reconstruct the wrapper with the modified inner type\n if (field instanceof z.ZodOptional) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return inner.optional() as unknown as StripZodDefault<T>;\n }\n if (field instanceof z.ZodNullable) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return inner.nullable() as unknown as StripZodDefault<T>;\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field as StripZodDefault<T>;\n}\n\n/**\n * Determines if a field will show validation errors when the user submits empty or invalid input.\n *\n * This is useful for form UIs to indicate which fields require valid user input (e.g., showing\n * asterisks, validation states). The key insight: **defaults are just initial values** - they\n * don't prevent validation errors if the user clears the field.\n *\n * **Real-world example:**\n * ```typescript\n * // Marital status field with default but validation rules\n * const maritalStatus = z.string().min(1).default('single');\n *\n * // Initial: field shows \"single\" (from default)\n * // User deletes the value → field is now empty string\n * // User submits form → validation fails because .min(1) rejects empty strings\n * // requiresValidInput(maritalStatus) → true (shows * indicator, validation error)\n * ```\n *\n * **How it works:**\n * 1. Removes `.default()` wrappers (defaults are initial values, not validation rules)\n * 2. Tests if the underlying schema accepts empty/invalid input:\n * - `undefined` (via `.optional()`)\n * - `null` (via `.nullable()`)\n * - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)\n * - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)\n * 3. Returns `true` if validation will fail, `false` if empty input is accepted\n *\n * @template T - The Zod type to check\n * @param field - The Zod field to check\n * @returns True if the field will show validation errors on empty/invalid input, false otherwise\n *\n * @example\n * User name field - required, no default\n * ```typescript\n * const userName = z.string().min(1);\n * requiresValidInput(userName); // true - will error if user submits empty\n * ```\n *\n * @example\n * Marital status - required WITH default\n * ```typescript\n * const maritalStatus = z.string().min(1).default('single');\n * requiresValidInput(maritalStatus); // true - will error if user clears and submits\n * ```\n *\n * @example\n * Age with default - requires valid input\n * ```typescript\n * const age = z.number().default(0);\n * requiresValidInput(age); // true - numbers reject empty strings\n * ```\n *\n * @example\n * Optional bio field - doesn't require input\n * ```typescript\n * const bio = z.string().optional();\n * requiresValidInput(bio); // false - user can leave empty\n * ```\n *\n * @example\n * String with default but NO validation - doesn't require input\n * ```typescript\n * const notes = z.string().default('N/A');\n * requiresValidInput(notes); // false - plain z.string() accepts empty strings\n * ```\n *\n * @example\n * Nullable field - doesn't require input\n * ```typescript\n * const middleName = z.string().nullable();\n * requiresValidInput(middleName); // false - user can leave null\n * ```\n *\n * @see {@link removeDefault} for understanding how defaults are handled\n * @see {@link getPrimitiveType} for understanding type unwrapping\n * @since 0.1.0\n */\nexport const requiresValidInput = <T extends z.ZodType>(field: T) => {\n const defaultRemovedField = removeDefault(field);\n if (!(defaultRemovedField instanceof z.ZodType)) {\n return false;\n }\n\n const undefinedResult = defaultRemovedField.safeParse(undefined).success;\n\n // Check if field accepts null (nullable)\n const nullResult = defaultRemovedField.safeParse(null).success;\n\n const primitiveType = getPrimitiveType(defaultRemovedField);\n\n const emptyStringResult =\n primitiveType.type === 'string' &&\n defaultRemovedField.safeParse('').success;\n\n const emptyArrayResult =\n primitiveType.type === 'array' && defaultRemovedField.safeParse([]).success;\n\n return (\n !undefinedResult && !nullResult && !emptyStringResult && !emptyArrayResult\n );\n};\n","import * as z from 'zod';\nimport { canUnwrap } from './schema';\nimport type { Simplify } from './types';\n\n/**\n * Extracts the default value from a Zod field, recursively unwrapping optional and nullable layers.\n *\n * This function traverses through wrapper types (like `ZodOptional`, `ZodNullable`) to find\n * the underlying `ZodDefault` and returns its default value. If no default is found, returns `undefined`.\n *\n * @template T - The Zod type to extract default from\n * @param field - The Zod field to extract default from\n * @returns The default value if present, undefined otherwise\n *\n * @example\n * Basic usage with default value\n * ```typescript\n * const field = z.string().default('hello');\n * const defaultValue = extractDefault(field);\n * // Result: 'hello'\n * ```\n *\n * @example\n * Unwrapping optional/nullable layers\n * ```typescript\n * const field = z.string().default('world').optional();\n * const defaultValue = extractDefault(field);\n * // Result: 'world' (unwraps optional to find default)\n * ```\n *\n * @example\n * Field without default\n * ```typescript\n * const field = z.string().optional();\n * const defaultValue = extractDefault(field);\n * // Result: undefined\n * ```\n *\n * @see {@link getSchemaDefaults} for extracting defaults from entire schemas\n * @since 0.1.0\n */\nexport function extractDefault<T extends z.ZodTypeAny>(\n field: T,\n): z.infer<T> | undefined {\n if (field instanceof z.ZodDefault) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field.def.defaultValue as z.infer<T>;\n }\n\n if (canUnwrap(field)) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return extractDefault(field.unwrap()) as z.infer<T>;\n }\n\n return undefined;\n}\n\n/**\n * Extracts default values from a Zod object schema while skipping fields without defaults.\n *\n * This function recursively traverses the schema and collects all fields that have\n * explicit default values defined. Fields without defaults are excluded from the result.\n *\n * **Important:** Nested defaults are NOT extracted unless the parent object also has\n * an explicit `.default()`. This is by design to match Zod's default value behavior.\n *\n * @template T - The Zod object schema type\n * @param schema - The Zod object schema to extract defaults from\n * @returns A partial object containing only fields with default values\n *\n * @example\n * Basic usage\n * ```typescript\n * const schema = z.object({\n * name: z.string().default('John'),\n * age: z.number(), // no default - will be skipped\n * email: z.string().email().optional(),\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { name: 'John' }\n * ```\n *\n * @example\n * Nested objects with defaults\n * ```typescript\n * const schema = z.object({\n * user: z.object({\n * name: z.string().default('Guest')\n * }).default({ name: 'Guest' }), // ✅ Extracted because parent has .default()\n *\n * settings: z.object({\n * theme: z.string().default('light')\n * }), // ❌ NOT extracted - parent has no .default()\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { user: { name: 'Guest' } }\n * ```\n *\n * @example\n * Unwrapping optional/nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string().default('Untitled').optional(),\n * count: z.number().default(0).nullable(),\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { title: 'Untitled', count: 0 }\n * ```\n *\n * @see {@link extractDefault} for extracting defaults from individual fields\n * @since 0.1.0\n */\nexport function getSchemaDefaults<T extends z.ZodObject>(\n schema: T,\n): Simplify<Partial<z.infer<T>>> {\n const defaults: Record<string, unknown> = {};\n\n for (const key in schema.shape) {\n const field = schema.shape[key];\n if (!field) continue;\n\n // Check if this field has an explicit default value\n const defaultValue = extractDefault(field);\n if (defaultValue !== undefined) {\n defaults[key] = defaultValue;\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return defaults as Partial<z.infer<T>>;\n}\n"]}
package/dist/index.mjs CHANGED
@@ -4,6 +4,23 @@ import * as z from 'zod';
4
4
  function canUnwrap(field) {
5
5
  return "unwrap" in field && typeof field.unwrap === "function";
6
6
  }
7
+ function unwrapUnion(field, options = {}) {
8
+ const { filterNullish = true } = options;
9
+ if (field instanceof z.ZodUnion) {
10
+ const unionOptions = [...field.def.options];
11
+ const filteredOptions = filterNullish ? unionOptions.filter(
12
+ (option) => !(option instanceof z.ZodNull) && !(option instanceof z.ZodUndefined)
13
+ ) : unionOptions;
14
+ return {
15
+ field: filteredOptions[0] || field,
16
+ union: filteredOptions
17
+ };
18
+ }
19
+ return {
20
+ field,
21
+ union: [field]
22
+ };
23
+ }
7
24
  var getPrimitiveType = (field) => {
8
25
  if (field instanceof z.ZodArray) {
9
26
  return field;
@@ -11,6 +28,9 @@ var getPrimitiveType = (field) => {
11
28
  if (canUnwrap(field)) {
12
29
  return getPrimitiveType(field.unwrap());
13
30
  }
31
+ if (field instanceof z.ZodUnion) {
32
+ return getPrimitiveType(unwrapUnion(field).field);
33
+ }
14
34
  return field;
15
35
  };
16
36
  function removeDefault(field) {
@@ -28,15 +48,12 @@ function removeDefault(field) {
28
48
  }
29
49
  return field;
30
50
  }
31
- var checkIfFieldIsRequired = (field) => {
32
- const undefinedResult = field.safeParse(void 0).success;
33
- if (undefinedResult) {
34
- return false;
35
- }
51
+ var requiresValidInput = (field) => {
36
52
  const defaultRemovedField = removeDefault(field);
37
53
  if (!(defaultRemovedField instanceof z.ZodType)) {
38
54
  return false;
39
55
  }
56
+ const undefinedResult = defaultRemovedField.safeParse(void 0).success;
40
57
  const nullResult = defaultRemovedField.safeParse(null).success;
41
58
  const primitiveType = getPrimitiveType(defaultRemovedField);
42
59
  const emptyStringResult = primitiveType.type === "string" && defaultRemovedField.safeParse("").success;
@@ -67,6 +84,6 @@ function getSchemaDefaults(schema) {
67
84
  return defaults;
68
85
  }
69
86
 
70
- export { canUnwrap, checkIfFieldIsRequired, extractDefault, getPrimitiveType, getSchemaDefaults, removeDefault };
87
+ export { canUnwrap, extractDefault, getPrimitiveType, getSchemaDefaults, removeDefault, requiresValidInput, unwrapUnion };
71
88
  //# sourceMappingURL=index.mjs.map
72
89
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/schema.ts","../src/defaults.ts"],"names":["z2"],"mappings":"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","file":"index.mjs","sourcesContent":["import * as z from 'zod';\n\n/**\n * Type representing a Zod type that has an unwrap method\n */\ntype Unwrappable = { unwrap: () => z.ZodTypeAny };\n\n/**\n * Type guard to check if a Zod field can be unwrapped (has wrapper types like optional, nullable, default).\n *\n * This checks whether a Zod type has an `unwrap()` method, which is present on wrapper types\n * like `ZodOptional`, `ZodNullable`, `ZodDefault`, and others.\n *\n * @param field - The Zod field to check\n * @returns True if the field has an unwrap method, false otherwise\n *\n * @example\n * ```typescript\n * const optionalField = z.string().optional();\n * console.log(canUnwrap(optionalField)); // true\n *\n * const plainField = z.string();\n * console.log(canUnwrap(plainField)); // false\n * ```\n *\n * @since 0.1.0\n */\nexport function canUnwrap(\n field: z.ZodTypeAny,\n): field is z.ZodTypeAny & Unwrappable {\n return 'unwrap' in field && typeof field.unwrap === 'function';\n}\n\n/**\n * Gets the underlying primitive type of a Zod field by recursively unwrapping wrapper types.\n *\n * This function removes wrapper layers (optional, nullable, default) to reveal the base type.\n * **Important:** It stops at array types without unwrapping them, treating arrays as primitives.\n *\n * @template T - The Zod type to unwrap\n * @param field - The Zod field to unwrap\n * @returns The unwrapped primitive Zod type\n *\n * @example\n * Unwrapping to string primitive\n * ```typescript\n * const field = z.string().optional().nullable();\n * const primitive = getPrimitiveType(field);\n * // Result: z.string() (unwrapped all wrappers)\n * ```\n *\n * @example\n * Stopping at array type\n * ```typescript\n * const field = z.array(z.string()).optional();\n * const primitive = getPrimitiveType(field);\n * // Result: z.array(z.string()) (stops at array, doesn't unwrap it)\n * ```\n *\n * @example\n * Unwrapping defaults\n * ```typescript\n * const field = z.number().default(0).optional();\n * const primitive = getPrimitiveType(field);\n * // Result: z.number()\n * ```\n *\n * @see {@link canUnwrap} for checking if a field can be unwrapped\n * @since 0.1.0\n */\nexport const getPrimitiveType = <T extends z.ZodTypeAny>(\n field: T,\n): z.ZodTypeAny => {\n // Stop at arrays - don't unwrap them\n if (field instanceof z.ZodArray) {\n return field;\n }\n\n if (canUnwrap(field)) {\n return getPrimitiveType(field.unwrap());\n }\n\n return field;\n};\n\ntype StripZodDefault<T> = T extends z.ZodDefault<infer Inner>\n ? StripZodDefault<Inner>\n : T extends z.ZodOptional<infer Inner>\n ? z.ZodOptional<StripZodDefault<Inner>>\n : T extends z.ZodNullable<infer Inner>\n ? z.ZodNullable<StripZodDefault<Inner>>\n : T;\n\n/**\n * Removes default values from a Zod field while preserving other wrapper types.\n *\n * This function recursively removes `ZodDefault` wrappers from a field, while maintaining\n * `optional()` and `nullable()` wrappers. Useful for scenarios where you want to check\n * field requirements without considering default values.\n *\n * @template T - The Zod type to process\n * @param field - The Zod field to remove defaults from\n * @returns The field without defaults but with optional/nullable preserved\n *\n * @example\n * Removing simple default\n * ```typescript\n * const field = z.string().default('hello');\n * const withoutDefault = removeDefault(field);\n * // Result: z.string()\n * ```\n *\n * @example\n * Preserving optional wrapper\n * ```typescript\n * const field = z.string().default('hello').optional();\n * const withoutDefault = removeDefault(field);\n * // Result: z.string().optional()\n * ```\n *\n * @example\n * Nested defaults\n * ```typescript\n * const field = z.string().default('inner').nullable().default('outer');\n * const withoutDefault = removeDefault(field);\n * // Result: z.string().nullable()\n * ```\n *\n * @see {@link checkIfFieldIsRequired} for usage with requirement checking\n * @since 0.1.0\n */\nexport function removeDefault<T extends z.ZodType>(\n field: T,\n): StripZodDefault<T> {\n if (field instanceof z.ZodDefault) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field.unwrap() as StripZodDefault<T>;\n }\n\n if ('innerType' in field.def && field.def.innerType instanceof z.ZodType) {\n const inner = removeDefault(field.def.innerType);\n // Reconstruct the wrapper with the modified inner type\n if (field instanceof z.ZodOptional) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return inner.optional() as unknown as StripZodDefault<T>;\n }\n if (field instanceof z.ZodNullable) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return inner.nullable() as unknown as StripZodDefault<T>;\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field as StripZodDefault<T>;\n}\n\n/**\n * Checks if a Zod field is truly required by testing multiple acceptance criteria.\n *\n * A field is considered **not required** if it accepts any of the following:\n * - `undefined` (via `.optional()` or `.default()`)\n * - `null` (via `.nullable()`)\n * - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)\n * - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)\n *\n * **Note:** Fields with `.default()` are considered not required since they'll have a value\n * even if the user doesn't provide one.\n *\n * @template T - The Zod type to check\n * @param field - The Zod field to check for required status\n * @returns True if the field is required, false otherwise\n *\n * @example\n * Required field\n * ```typescript\n * const field = z.string().min(1);\n * console.log(checkIfFieldIsRequired(field)); // true\n * ```\n *\n * @example\n * Optional field (not required)\n * ```typescript\n * const field = z.string().optional();\n * console.log(checkIfFieldIsRequired(field)); // false\n * ```\n *\n * @example\n * Field with default (not required)\n * ```typescript\n * const field = z.string().default('hello');\n * console.log(checkIfFieldIsRequired(field)); // false\n * ```\n *\n * @example\n * String without min length (not required - accepts empty string)\n * ```typescript\n * const field = z.string();\n * console.log(checkIfFieldIsRequired(field)); // false\n * ```\n *\n * @example\n * String with nonempty (required)\n * ```typescript\n * const field = z.string().nonempty();\n * console.log(checkIfFieldIsRequired(field)); // true\n * ```\n *\n * @example\n * Nullable field (not required)\n * ```typescript\n * const field = z.number().nullable();\n * console.log(checkIfFieldIsRequired(field)); // false\n * ```\n *\n * @see {@link removeDefault} for understanding how defaults are handled\n * @see {@link getPrimitiveType} for understanding type unwrapping\n * @since 0.1.0\n */\nexport const checkIfFieldIsRequired = <T extends z.ZodType>(field: T) => {\n // First check the original field for undefined - this catches fields with defaults\n const undefinedResult = field.safeParse(undefined).success;\n if (undefinedResult) {\n return false;\n }\n\n const defaultRemovedField = removeDefault(field);\n\n if (!(defaultRemovedField instanceof z.ZodType)) {\n return false;\n }\n\n // Check if field accepts null (nullable)\n const nullResult = defaultRemovedField.safeParse(null).success;\n\n const primitiveType = getPrimitiveType(defaultRemovedField);\n\n const emptyStringResult =\n primitiveType.type === 'string' &&\n defaultRemovedField.safeParse('').success;\n\n const emptyArrayResult =\n primitiveType.type === 'array' && defaultRemovedField.safeParse([]).success;\n\n return (\n !undefinedResult && !nullResult && !emptyStringResult && !emptyArrayResult\n );\n};\n","import * as z from 'zod';\nimport { canUnwrap } from './schema';\nimport type { Simplify } from './types';\n\n/**\n * Extracts the default value from a Zod field, recursively unwrapping optional and nullable layers.\n *\n * This function traverses through wrapper types (like `ZodOptional`, `ZodNullable`) to find\n * the underlying `ZodDefault` and returns its default value. If no default is found, returns `undefined`.\n *\n * @template T - The Zod type to extract default from\n * @param field - The Zod field to extract default from\n * @returns The default value if present, undefined otherwise\n *\n * @example\n * Basic usage with default value\n * ```typescript\n * const field = z.string().default('hello');\n * const defaultValue = extractDefault(field);\n * // Result: 'hello'\n * ```\n *\n * @example\n * Unwrapping optional/nullable layers\n * ```typescript\n * const field = z.string().default('world').optional();\n * const defaultValue = extractDefault(field);\n * // Result: 'world' (unwraps optional to find default)\n * ```\n *\n * @example\n * Field without default\n * ```typescript\n * const field = z.string().optional();\n * const defaultValue = extractDefault(field);\n * // Result: undefined\n * ```\n *\n * @see {@link getSchemaDefaults} for extracting defaults from entire schemas\n * @since 0.1.0\n */\nexport function extractDefault<T extends z.ZodTypeAny>(\n field: T,\n): z.infer<T> | undefined {\n if (field instanceof z.ZodDefault) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field.def.defaultValue as z.infer<T>;\n }\n\n if (canUnwrap(field)) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return extractDefault(field.unwrap()) as z.infer<T>;\n }\n\n return undefined;\n}\n\n/**\n * Extracts default values from a Zod object schema while skipping fields without defaults.\n *\n * This function recursively traverses the schema and collects all fields that have\n * explicit default values defined. Fields without defaults are excluded from the result.\n *\n * **Important:** Nested defaults are NOT extracted unless the parent object also has\n * an explicit `.default()`. This is by design to match Zod's default value behavior.\n *\n * @template T - The Zod object schema type\n * @param schema - The Zod object schema to extract defaults from\n * @returns A partial object containing only fields with default values\n *\n * @example\n * Basic usage\n * ```typescript\n * const schema = z.object({\n * name: z.string().default('John'),\n * age: z.number(), // no default - will be skipped\n * email: z.string().email().optional(),\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { name: 'John' }\n * ```\n *\n * @example\n * Nested objects with defaults\n * ```typescript\n * const schema = z.object({\n * user: z.object({\n * name: z.string().default('Guest')\n * }).default({ name: 'Guest' }), // ✅ Extracted because parent has .default()\n *\n * settings: z.object({\n * theme: z.string().default('light')\n * }), // ❌ NOT extracted - parent has no .default()\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { user: { name: 'Guest' } }\n * ```\n *\n * @example\n * Unwrapping optional/nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string().default('Untitled').optional(),\n * count: z.number().default(0).nullable(),\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { title: 'Untitled', count: 0 }\n * ```\n *\n * @see {@link extractDefault} for extracting defaults from individual fields\n * @since 0.1.0\n */\nexport function getSchemaDefaults<T extends z.ZodObject>(\n schema: T,\n): Simplify<Partial<z.infer<T>>> {\n const defaults: Record<string, unknown> = {};\n\n for (const key in schema.shape) {\n const field = schema.shape[key];\n if (!field) continue;\n\n // Check if this field has an explicit default value\n const defaultValue = extractDefault(field);\n if (defaultValue !== undefined) {\n defaults[key] = defaultValue;\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return defaults as Partial<z.infer<T>>;\n}\n"]}
1
+ {"version":3,"sources":["../src/schema.ts","../src/defaults.ts"],"names":["z2"],"mappings":"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","file":"index.mjs","sourcesContent":["import * as z from 'zod';\n\n/**\n * Type representing a Zod type that has an unwrap method\n */\ntype Unwrappable = { unwrap: () => z.ZodTypeAny };\n\n/**\n * Type guard to check if a Zod field can be unwrapped (has wrapper types like optional, nullable, default).\n *\n * This checks whether a Zod type has an `unwrap()` method, which is present on wrapper types\n * like `ZodOptional`, `ZodNullable`, `ZodDefault`, and others.\n *\n * @param field - The Zod field to check\n * @returns True if the field has an unwrap method, false otherwise\n *\n * @example\n * ```typescript\n * const optionalField = z.string().optional();\n * console.log(canUnwrap(optionalField)); // true\n *\n * const plainField = z.string();\n * console.log(canUnwrap(plainField)); // false\n * ```\n *\n * @since 0.1.0\n */\nexport function canUnwrap(\n field: z.ZodTypeAny,\n): field is z.ZodTypeAny & Unwrappable {\n return 'unwrap' in field && typeof field.unwrap === 'function';\n}\n\n/**\n * Unwraps a ZodUnion type and returns the first field and all union options.\n *\n * This function extracts the individual type options from a union type.\n * By default, it filters out `ZodNull` and `ZodUndefined` types, returning only\n * the meaningful type options. You can disable this filtering to get all options.\n *\n * @template T - The Zod type to unwrap\n * @param field - The Zod field (union or single type)\n * @param options - Configuration options\n * @param options.filterNullish - Whether to filter out null and undefined types (default: true)\n * @returns Object with `field` (first option) and `union` (all options array)\n *\n * @example\n * Basic union unwrapping\n * ```typescript\n * const field = z.union([z.string(), z.number()]);\n * const result = unwrapUnion(field);\n * // Result: { field: z.string(), union: [z.string(), z.number()] }\n * ```\n *\n * @example\n * Union with null (filtered by default)\n * ```typescript\n * const field = z.union([z.string(), z.null()]);\n * const result = unwrapUnion(field);\n * // Result: { field: z.string(), union: [z.string()] }\n * ```\n *\n * @example\n * Union with null (keep all options)\n * ```typescript\n * const field = z.union([z.string(), z.null()]);\n * const result = unwrapUnion(field, { filterNullish: false });\n * // Result: { field: z.string(), union: [z.string(), z.null()] }\n * ```\n *\n * @example\n * Non-union type (returns single field)\n * ```typescript\n * const field = z.string();\n * const result = unwrapUnion(field);\n * // Result: { field: z.string(), union: [z.string()] }\n * ```\n *\n * @example\n * Nullable as union\n * ```typescript\n * const field = z.string().nullable(); // This is z.union([z.string(), z.null()])\n * const result = unwrapUnion(field);\n * // Result: { field: z.string(), union: [z.string()] } (null filtered out)\n * ```\n *\n * @example\n * Using the first field for type checking\n * ```typescript\n * const field = z.union([z.string(), z.number()]);\n * const { field: firstField, union } = unwrapUnion(field);\n * if (firstField instanceof z.ZodString) {\n * console.log('First type is string');\n * }\n * ```\n *\n * @see {@link getPrimitiveType} for unwrapping wrapper types\n * @since 0.1.0\n */\nexport function unwrapUnion<T extends z.ZodTypeAny>(\n field: T,\n options: { filterNullish?: boolean } = {},\n): { field: z.ZodTypeAny; union: z.ZodTypeAny[] } {\n const { filterNullish = true } = options;\n\n if (field instanceof z.ZodUnion) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const unionOptions = [...field.def.options] as z.ZodTypeAny[];\n\n const filteredOptions = filterNullish\n ? unionOptions.filter(\n (option) =>\n !(option instanceof z.ZodNull) &&\n !(option instanceof z.ZodUndefined),\n )\n : unionOptions;\n\n return {\n field: filteredOptions[0] || field,\n union: filteredOptions,\n };\n }\n\n // If it's not a union, return the field itself\n return {\n field,\n union: [field],\n };\n}\n\n/**\n * Gets the underlying primitive type of a Zod field by recursively unwrapping wrapper types.\n *\n * This function removes wrapper layers (optional, nullable, default) to reveal the base type.\n * **Important:** It stops at array types without unwrapping them, treating arrays as primitives.\n *\n * @template T - The Zod type to unwrap\n * @param field - The Zod field to unwrap\n * @returns The unwrapped primitive Zod type\n *\n * @example\n * Unwrapping to string primitive\n * ```typescript\n * const field = z.string().optional().nullable();\n * const primitive = getPrimitiveType(field);\n * // Result: z.string() (unwrapped all wrappers)\n * ```\n *\n * @example\n * Stopping at array type\n * ```typescript\n * const field = z.array(z.string()).optional();\n * const primitive = getPrimitiveType(field);\n * // Result: z.array(z.string()) (stops at array, doesn't unwrap it)\n * ```\n *\n * @example\n * Unwrapping defaults\n * ```typescript\n * const field = z.number().default(0).optional();\n * const primitive = getPrimitiveType(field);\n * // Result: z.number()\n * ```\n *\n * @see {@link canUnwrap} for checking if a field can be unwrapped\n * @since 0.1.0\n */\nexport const getPrimitiveType = <T extends z.ZodType>(\n field: T,\n): z.ZodTypeAny => {\n // Stop at arrays - don't unwrap them\n if (field instanceof z.ZodArray) {\n return field;\n }\n\n if (canUnwrap(field)) {\n return getPrimitiveType(field.unwrap());\n }\n\n if (field instanceof z.ZodUnion) {\n return getPrimitiveType(unwrapUnion(field).field);\n }\n\n return field;\n};\n\ntype StripZodDefault<T> = T extends z.ZodDefault<infer Inner>\n ? StripZodDefault<Inner>\n : T extends z.ZodOptional<infer Inner>\n ? z.ZodOptional<StripZodDefault<Inner>>\n : T extends z.ZodNullable<infer Inner>\n ? z.ZodNullable<StripZodDefault<Inner>>\n : T;\n\n/**\n * Removes default values from a Zod field while preserving other wrapper types.\n *\n * This function recursively removes `ZodDefault` wrappers from a field, while maintaining\n * `optional()` and `nullable()` wrappers. Useful for scenarios where you want to check\n * field requirements without considering default values.\n *\n * @template T - The Zod type to process\n * @param field - The Zod field to remove defaults from\n * @returns The field without defaults but with optional/nullable preserved\n *\n * @example\n * Removing simple default\n * ```typescript\n * const field = z.string().default('hello');\n * const withoutDefault = removeDefault(field);\n * // Result: z.string()\n * ```\n *\n * @example\n * Preserving optional wrapper\n * ```typescript\n * const field = z.string().default('hello').optional();\n * const withoutDefault = removeDefault(field);\n * // Result: z.string().optional()\n * ```\n *\n * @example\n * Nested defaults\n * ```typescript\n * const field = z.string().default('inner').nullable().default('outer');\n * const withoutDefault = removeDefault(field);\n * // Result: z.string().nullable()\n * ```\n *\n * @see {@link requiresValidInput} for usage with requirement checking\n * @since 0.1.0\n */\nexport function removeDefault<T extends z.ZodType>(\n field: T,\n): StripZodDefault<T> {\n if (field instanceof z.ZodDefault) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field.unwrap() as StripZodDefault<T>;\n }\n\n if ('innerType' in field.def && field.def.innerType instanceof z.ZodType) {\n const inner = removeDefault(field.def.innerType);\n // Reconstruct the wrapper with the modified inner type\n if (field instanceof z.ZodOptional) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return inner.optional() as unknown as StripZodDefault<T>;\n }\n if (field instanceof z.ZodNullable) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return inner.nullable() as unknown as StripZodDefault<T>;\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field as StripZodDefault<T>;\n}\n\n/**\n * Determines if a field will show validation errors when the user submits empty or invalid input.\n *\n * This is useful for form UIs to indicate which fields require valid user input (e.g., showing\n * asterisks, validation states). The key insight: **defaults are just initial values** - they\n * don't prevent validation errors if the user clears the field.\n *\n * **Real-world example:**\n * ```typescript\n * // Marital status field with default but validation rules\n * const maritalStatus = z.string().min(1).default('single');\n *\n * // Initial: field shows \"single\" (from default)\n * // User deletes the value → field is now empty string\n * // User submits form → validation fails because .min(1) rejects empty strings\n * // requiresValidInput(maritalStatus) → true (shows * indicator, validation error)\n * ```\n *\n * **How it works:**\n * 1. Removes `.default()` wrappers (defaults are initial values, not validation rules)\n * 2. Tests if the underlying schema accepts empty/invalid input:\n * - `undefined` (via `.optional()`)\n * - `null` (via `.nullable()`)\n * - Empty string (plain `z.string()` without `.min(1)` or `.nonempty()`)\n * - Empty array (plain `z.array()` without `.min(1)` or `.nonempty()`)\n * 3. Returns `true` if validation will fail, `false` if empty input is accepted\n *\n * @template T - The Zod type to check\n * @param field - The Zod field to check\n * @returns True if the field will show validation errors on empty/invalid input, false otherwise\n *\n * @example\n * User name field - required, no default\n * ```typescript\n * const userName = z.string().min(1);\n * requiresValidInput(userName); // true - will error if user submits empty\n * ```\n *\n * @example\n * Marital status - required WITH default\n * ```typescript\n * const maritalStatus = z.string().min(1).default('single');\n * requiresValidInput(maritalStatus); // true - will error if user clears and submits\n * ```\n *\n * @example\n * Age with default - requires valid input\n * ```typescript\n * const age = z.number().default(0);\n * requiresValidInput(age); // true - numbers reject empty strings\n * ```\n *\n * @example\n * Optional bio field - doesn't require input\n * ```typescript\n * const bio = z.string().optional();\n * requiresValidInput(bio); // false - user can leave empty\n * ```\n *\n * @example\n * String with default but NO validation - doesn't require input\n * ```typescript\n * const notes = z.string().default('N/A');\n * requiresValidInput(notes); // false - plain z.string() accepts empty strings\n * ```\n *\n * @example\n * Nullable field - doesn't require input\n * ```typescript\n * const middleName = z.string().nullable();\n * requiresValidInput(middleName); // false - user can leave null\n * ```\n *\n * @see {@link removeDefault} for understanding how defaults are handled\n * @see {@link getPrimitiveType} for understanding type unwrapping\n * @since 0.1.0\n */\nexport const requiresValidInput = <T extends z.ZodType>(field: T) => {\n const defaultRemovedField = removeDefault(field);\n if (!(defaultRemovedField instanceof z.ZodType)) {\n return false;\n }\n\n const undefinedResult = defaultRemovedField.safeParse(undefined).success;\n\n // Check if field accepts null (nullable)\n const nullResult = defaultRemovedField.safeParse(null).success;\n\n const primitiveType = getPrimitiveType(defaultRemovedField);\n\n const emptyStringResult =\n primitiveType.type === 'string' &&\n defaultRemovedField.safeParse('').success;\n\n const emptyArrayResult =\n primitiveType.type === 'array' && defaultRemovedField.safeParse([]).success;\n\n return (\n !undefinedResult && !nullResult && !emptyStringResult && !emptyArrayResult\n );\n};\n","import * as z from 'zod';\nimport { canUnwrap } from './schema';\nimport type { Simplify } from './types';\n\n/**\n * Extracts the default value from a Zod field, recursively unwrapping optional and nullable layers.\n *\n * This function traverses through wrapper types (like `ZodOptional`, `ZodNullable`) to find\n * the underlying `ZodDefault` and returns its default value. If no default is found, returns `undefined`.\n *\n * @template T - The Zod type to extract default from\n * @param field - The Zod field to extract default from\n * @returns The default value if present, undefined otherwise\n *\n * @example\n * Basic usage with default value\n * ```typescript\n * const field = z.string().default('hello');\n * const defaultValue = extractDefault(field);\n * // Result: 'hello'\n * ```\n *\n * @example\n * Unwrapping optional/nullable layers\n * ```typescript\n * const field = z.string().default('world').optional();\n * const defaultValue = extractDefault(field);\n * // Result: 'world' (unwraps optional to find default)\n * ```\n *\n * @example\n * Field without default\n * ```typescript\n * const field = z.string().optional();\n * const defaultValue = extractDefault(field);\n * // Result: undefined\n * ```\n *\n * @see {@link getSchemaDefaults} for extracting defaults from entire schemas\n * @since 0.1.0\n */\nexport function extractDefault<T extends z.ZodTypeAny>(\n field: T,\n): z.infer<T> | undefined {\n if (field instanceof z.ZodDefault) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return field.def.defaultValue as z.infer<T>;\n }\n\n if (canUnwrap(field)) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return extractDefault(field.unwrap()) as z.infer<T>;\n }\n\n return undefined;\n}\n\n/**\n * Extracts default values from a Zod object schema while skipping fields without defaults.\n *\n * This function recursively traverses the schema and collects all fields that have\n * explicit default values defined. Fields without defaults are excluded from the result.\n *\n * **Important:** Nested defaults are NOT extracted unless the parent object also has\n * an explicit `.default()`. This is by design to match Zod's default value behavior.\n *\n * @template T - The Zod object schema type\n * @param schema - The Zod object schema to extract defaults from\n * @returns A partial object containing only fields with default values\n *\n * @example\n * Basic usage\n * ```typescript\n * const schema = z.object({\n * name: z.string().default('John'),\n * age: z.number(), // no default - will be skipped\n * email: z.string().email().optional(),\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { name: 'John' }\n * ```\n *\n * @example\n * Nested objects with defaults\n * ```typescript\n * const schema = z.object({\n * user: z.object({\n * name: z.string().default('Guest')\n * }).default({ name: 'Guest' }), // ✅ Extracted because parent has .default()\n *\n * settings: z.object({\n * theme: z.string().default('light')\n * }), // ❌ NOT extracted - parent has no .default()\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { user: { name: 'Guest' } }\n * ```\n *\n * @example\n * Unwrapping optional/nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string().default('Untitled').optional(),\n * count: z.number().default(0).nullable(),\n * });\n *\n * const defaults = getSchemaDefaults(schema);\n * // Result: { title: 'Untitled', count: 0 }\n * ```\n *\n * @see {@link extractDefault} for extracting defaults from individual fields\n * @since 0.1.0\n */\nexport function getSchemaDefaults<T extends z.ZodObject>(\n schema: T,\n): Simplify<Partial<z.infer<T>>> {\n const defaults: Record<string, unknown> = {};\n\n for (const key in schema.shape) {\n const field = schema.shape[key];\n if (!field) continue;\n\n // Check if this field has an explicit default value\n const defaultValue = extractDefault(field);\n if (defaultValue !== undefined) {\n defaults[key] = defaultValue;\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return defaults as Partial<z.infer<T>>;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zod-utils/core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Pure TypeScript utilities for Zod schema manipulation and default extraction",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -28,6 +28,8 @@
28
28
  "test": "vitest run",
29
29
  "test:watch": "vitest",
30
30
  "test:coverage": "vitest run --coverage",
31
+ "bench": "vitest bench --run",
32
+ "bench:watch": "vitest bench",
31
33
  "prepublishOnly": "npm run build"
32
34
  },
33
35
  "keywords": [