334 lines
12 KiB
TypeScript
334 lines
12 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { z } from 'zod';
|
|
import { deepRemoveDefaults } from '../../../src/utils/zod/deep-remove-defaults';
|
|
|
|
describe('deepNoDefaults', () => {
|
|
describe('object field behavior', () => {
|
|
it('strips defaulted object fields', () => {
|
|
const schema = z.object({
|
|
string: z.string().default('foo'),
|
|
});
|
|
const result = deepRemoveDefaults(schema).parse({});
|
|
expect(result.string).toBeUndefined();
|
|
});
|
|
|
|
it('strips prefaulted object fields', () => {
|
|
const schema = z.object({
|
|
string: z.string().prefault('foo'),
|
|
});
|
|
const result = deepRemoveDefaults(schema).parse({});
|
|
expect(result.string).toBeUndefined();
|
|
});
|
|
|
|
it('keeps non-defaulted object fields required', () => {
|
|
const schema = z.object({
|
|
required: z.string(),
|
|
});
|
|
const result = deepRemoveDefaults(schema).safeParse({});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('keeps explicitly optional fields optional', () => {
|
|
const schema = z.object({
|
|
maybe: z.string().optional(),
|
|
});
|
|
const result = deepRemoveDefaults(schema).safeParse({});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.maybe).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('does not double-wrap an already optional field when stripping', () => {
|
|
const schema = z.object({
|
|
value: z.string().optional().default('x'),
|
|
});
|
|
const stripped = deepRemoveDefaults(schema);
|
|
const field = stripped.shape.value;
|
|
expect(field).toBeInstanceOf(z.ZodOptional);
|
|
expect(field.unwrap()).toBeInstanceOf(z.ZodString);
|
|
expect(stripped.parse({}).value).toBeUndefined();
|
|
});
|
|
|
|
it('makes union-defaulted fields optional', () => {
|
|
const schema = z.object({
|
|
x: z.union([z.string().default(''), z.number().default(0)]),
|
|
});
|
|
const parsed = deepRemoveDefaults(schema).safeParse({});
|
|
expect(parsed.success).toBe(true);
|
|
if (parsed.success) {
|
|
expect(parsed.data.x).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('keeps union fields required when no union option has defaults', () => {
|
|
const schema = z.object({
|
|
x: z.union([z.string(), z.number()]),
|
|
});
|
|
const parsed = deepRemoveDefaults(schema).safeParse({});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it('detects defaults under nullable wrappers for optionalization', () => {
|
|
const schema = z.object({
|
|
maybe: z.string().default('x').nullable(),
|
|
});
|
|
const result = deepRemoveDefaults(schema).safeParse({});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.maybe).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('detects defaults under readonly wrappers for optionalization', () => {
|
|
const schema = z.object({
|
|
readOnlyValue: z.string().default('x').readonly(),
|
|
});
|
|
const result = deepRemoveDefaults(schema).safeParse({});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.readOnlyValue).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('detects defaults under nonoptional wrappers for optionalization', () => {
|
|
const schema = z.object({
|
|
strictValue: z.string().default('x').nonoptional(),
|
|
});
|
|
const result = deepRemoveDefaults(schema).safeParse({});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.strictValue).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('detects defaults under pipe wrappers for optionalization', () => {
|
|
const schema = z.object({
|
|
piped: z.string().default('x').pipe(z.string().min(1)),
|
|
});
|
|
const result = deepRemoveDefaults(schema).safeParse({});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.piped).toBeUndefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('root wrapper stripping', () => {
|
|
it('strips a root default wrapper', () => {
|
|
const schema = z.string().default('foo');
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse(undefined).success).toBe(false);
|
|
expect(stripped.parse('bar')).toBe('bar');
|
|
});
|
|
|
|
it('strips a root prefault wrapper', () => {
|
|
const schema = z.string().prefault('foo');
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse(undefined).success).toBe(false);
|
|
expect(stripped.parse('bar')).toBe('bar');
|
|
});
|
|
|
|
it('returns leaf schemas as-is when no changes are needed', () => {
|
|
const schema = z.string().min(1);
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped).toBe(schema);
|
|
expect(stripped.safeParse('').success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('container schemas', () => {
|
|
it('strips defaults from array element schemas', () => {
|
|
const schema = z.array(z.string().default('foo'));
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse([undefined]).success).toBe(false);
|
|
expect(stripped.safeParse(['ok']).success).toBe(true);
|
|
});
|
|
|
|
it('still enforces array length constraints', () => {
|
|
expect(deepRemoveDefaults(z.number().array().min(1)).safeParse([]).success).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
deepRemoveDefaults(z.number().array().max(1)).safeParse([1, 2]).success,
|
|
).toBe(false);
|
|
expect(
|
|
deepRemoveDefaults(z.number().array().length(1)).safeParse([]).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('strips defaults from tuple items when tuple has no rest', () => {
|
|
const schema = z.tuple([z.string().default('foo')]);
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse([]).success).toBe(false);
|
|
expect(stripped.safeParse([undefined]).success).toBe(false);
|
|
expect(stripped.safeParse(['ok']).success).toBe(true);
|
|
});
|
|
|
|
it('strips defaults from tuple items and tuple rest', () => {
|
|
const schema = z.tuple([z.string().default('foo')], z.number().default(1));
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse([undefined]).success).toBe(false);
|
|
expect(stripped.safeParse(['ok', undefined]).success).toBe(false);
|
|
expect(stripped.safeParse(['ok', 2]).success).toBe(true);
|
|
});
|
|
|
|
it('strips defaults from all union options', () => {
|
|
const schema = z.union([z.string().default('foo'), z.number().default(1)]);
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse(undefined).success).toBe(false);
|
|
expect(stripped.safeParse('ok').success).toBe(true);
|
|
expect(stripped.safeParse(2).success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('recursive and cache behavior', () => {
|
|
it('handles lazy schemas while stripping nested defaults', () => {
|
|
const schema = z.lazy(() =>
|
|
z.object({
|
|
value: z.string().default('foo'),
|
|
}),
|
|
);
|
|
const stripped = deepRemoveDefaults(schema);
|
|
const parsed = stripped.safeParse({});
|
|
expect(parsed.success).toBe(true);
|
|
if (parsed.success) {
|
|
expect(parsed.data.value).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('handles getter-based recursive objects without stack overflow', () => {
|
|
const categorySchema = z.object({
|
|
name: z.string().default('root'),
|
|
get children() {
|
|
return z.array(categorySchema).default([]);
|
|
},
|
|
});
|
|
|
|
const stripped = deepRemoveDefaults(categorySchema);
|
|
const result = stripped.safeParse({});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.name).toBeUndefined();
|
|
expect(result.data.children).toBeUndefined();
|
|
}
|
|
|
|
// Force recursive traversal so the forward-reference lazy callback is executed.
|
|
const nested = stripped.safeParse({
|
|
children: [
|
|
{
|
|
children: [{}],
|
|
},
|
|
],
|
|
});
|
|
expect(nested.success).toBe(true);
|
|
});
|
|
|
|
it('handles self-referential lazy schemas in default detection', () => {
|
|
const self: z.ZodType = z.lazy(() => self);
|
|
const schema = z.object({
|
|
node: self,
|
|
});
|
|
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.shape.node).toBeInstanceOf(z.ZodLazy);
|
|
expect(stripped.shape.node).not.toBeInstanceOf(z.ZodOptional);
|
|
});
|
|
|
|
it('reuses cached child transforms for shared schema instances', () => {
|
|
const shared = z.string().default('foo');
|
|
const schema = z.object({
|
|
a: shared,
|
|
b: shared,
|
|
});
|
|
|
|
const stripped = deepRemoveDefaults(schema);
|
|
const a = stripped.shape.a;
|
|
const b = stripped.shape.b;
|
|
expect(a).toBeInstanceOf(z.ZodOptional);
|
|
expect(b).toBeInstanceOf(z.ZodOptional);
|
|
expect(a.unwrap()).toBe(b.unwrap());
|
|
});
|
|
});
|
|
|
|
describe('pipe and single-child wrappers', () => {
|
|
it('strips defaults from pipe output schemas', () => {
|
|
const schema = z.string().optional().pipe(z.string().default('bar'));
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse(undefined).success).toBe(false);
|
|
expect(stripped.safeParse('ok').success).toBe(true);
|
|
});
|
|
|
|
it('strips defaults inside optional wrappers', () => {
|
|
const schema = z.string().default('foo').optional();
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse(undefined).success).toBe(true);
|
|
expect(stripped.safeParse('ok').success).toBe(true);
|
|
expect(stripped.safeParse(1).success).toBe(false);
|
|
});
|
|
|
|
it('strips defaults inside nullable wrappers', () => {
|
|
const schema = z.string().default('foo').nullable();
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse(null).success).toBe(true);
|
|
expect(stripped.safeParse(undefined).success).toBe(false);
|
|
});
|
|
|
|
it('strips defaults inside readonly wrappers', () => {
|
|
const schema = z.string().default('foo').readonly();
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse('ok').success).toBe(true);
|
|
expect(stripped.safeParse(undefined).success).toBe(false);
|
|
});
|
|
|
|
it('strips defaults inside nonoptional wrappers', () => {
|
|
const schema = z.string().optional().default('foo').nonoptional();
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse(undefined).success).toBe(false);
|
|
expect(stripped.safeParse('ok').success).toBe(true);
|
|
});
|
|
|
|
it('strips defaults inside catch wrappers', () => {
|
|
const schema = z.string().default('foo').catch('fallback');
|
|
const stripped = deepRemoveDefaults(schema);
|
|
const parsed = stripped.safeParse(undefined);
|
|
expect(parsed.success).toBe(true);
|
|
if (parsed.success) {
|
|
expect(parsed.data).toBe('fallback');
|
|
}
|
|
});
|
|
|
|
it('strips defaults inside success wrappers', () => {
|
|
const schema = z.success(z.string().default('foo'));
|
|
const stripped = deepRemoveDefaults(schema);
|
|
expect(stripped.safeParse(undefined).success).toBe(false);
|
|
expect(stripped.parse('ok')).toBe(true);
|
|
});
|
|
|
|
it('strips defaults inside promise wrappers', async () => {
|
|
const schema = z.promise(z.string().default('foo'));
|
|
const stripped = deepRemoveDefaults(schema);
|
|
|
|
const missing = await stripped.safeParseAsync(Promise.resolve(undefined));
|
|
expect(missing.success).toBe(false);
|
|
|
|
const present = await stripped.safeParseAsync(Promise.resolve('ok'));
|
|
expect(present.success).toBe(true);
|
|
if (present.success) {
|
|
expect(present.data).toBe('ok');
|
|
}
|
|
});
|
|
|
|
it('throws when a wrapper unwraps to a non-classic schema value', () => {
|
|
const schema = z.string().nullable();
|
|
Object.defineProperty(schema, 'unwrap', {
|
|
value: () => ({}) as unknown as z.core.$ZodType,
|
|
});
|
|
|
|
expect(() => deepRemoveDefaults(schema)).toThrowError(
|
|
'deepRemoveDefaults supports full zod schemas only',
|
|
);
|
|
});
|
|
});
|
|
});
|