Repository: samchungy/zod-openapi
Pull request #352 (2024-10-30 05:35:17)

Optimise Optional Determining Logic
This also caught that we were treating `.catch()` incorrectly. This changes the `.catch()` behaviour to treat it the same as `.default()`.


Patch:
diff --git a/README.md b/README.md
index 6089aef..4e72dbd 100644
--- a/README.md
+++ b/README.md
@@ -469,7 +469,7 @@ createDocument({
 
 ##### Zod Effects
 
-`.transform()`, `.default()` and `.pipe()` are complicated because they technically comprise of two types (input & output). This means that we need to understand which type you are creating. In particular with transform it is very difficult to infer the output type. This library will automatically select which _type_ to use by checking how the schema is used based on the following rules:
+`.transform()`, `.catch()`, `.default()` and `.pipe()` are complicated because they technically comprise of two types (input & output). This means that we need to understand which type you are creating. In particular with transform it is very difficult to infer the output type. This library will automatically select which _type_ to use by checking how the schema is used based on the following rules:
 
 _Input_: Request Bodies, Request Parameters, Headers
 
@@ -694,6 +694,7 @@ For example in `z.string().nullable()` will be rendered differently
 - ZodBoolean
 - ZodBranded
 - ZodCatch
+  - Treated as ZodDefault
 - ZodDate
   - `type` is mapped as `string` by default
 - ZodDefault
diff --git a/src/create/parameters.ts b/src/create/parameters.ts
index aacb9c5..32347e2 100644
--- a/src/create/parameters.ts
+++ b/src/create/parameters.ts
@@ -10,7 +10,6 @@ import type {
   ZodOpenApiParameters,
 } from './document';
 import { type SchemaState, createSchema } from './schema';
-import { isOptionalSchema } from './schema/parsers/optional';
 
 export const createComponentParamRef = (ref: string) =>
   `#/components/parameters/${ref}`;
@@ -30,7 +29,8 @@ export const createBaseParameter = (
     documentOptions,
   };
   const schemaObject = createSchema(schema, state, [...subpath, 'schema']);
-  const required = !isOptionalSchema(schema, state)?.optional;
+  const required = !schema.isOptional();
+
   const description =
     schema._def.openapi?.description ?? schema._def.description;
 
diff --git a/src/create/responses.ts b/src/create/responses.ts
index 9486375..4d487b6 100644
--- a/src/create/responses.ts
+++ b/src/create/responses.ts
@@ -14,7 +14,6 @@ import type {
   ZodOpenApiResponsesObject,
 } from './document';
 import { type SchemaState, createSchema } from './schema';
-import { isOptionalSchema } from './schema/parsers/optional';
 import { isISpecificationExtension } from './specificationExtension';
 
 export const createResponseHeaders = (
@@ -90,7 +89,9 @@ export const createBaseHeader = (
     documentOptions,
   };
   const schemaObject = createSchema(schema, state, ['header']);
-  const required = !isOptionalSchema(schema, state)?.optional;
+  const optionalResult = schema.safeParse(undefined);
+
+  const required = !optionalResult.success || optionalResult !== undefined;
   return {
     ...rest,
     ...(schema && { schema: schemaObject }),
diff --git a/src/create/schema/parsers/catch.ts b/src/create/schema/parsers/catch.ts
index 79c56e9..dba6dec 100644
--- a/src/create/schema/parsers/catch.ts
+++ b/src/create/schema/parsers/catch.ts
@@ -1,12 +1,31 @@
 import type { ZodCatch, ZodTypeAny } from 'zod';
 
+import type { oas31 } from '../../../../dist';
 import {
   type Schema,
   type SchemaState,
   createSchemaObject,
 } from '../../schema';
+import { enhanceWithMetadata } from '../metadata';
 
 export const createCatchSchema = <T extends ZodTypeAny>(
   zodCatch: ZodCatch<T>,
   state: SchemaState,
-): Schema => createSchemaObject(zodCatch._def.innerType, state, ['catch']);
+): Schema => {
+  const schemaObject = createSchemaObject(zodCatch._def.innerType, state, [
+    'default',
+  ]);
+
+  const catchResult = zodCatch.safeParse(undefined);
+
+  const maybeDefaultValue: Pick<oas31.SchemaObject, 'default'> | undefined =
+    catchResult.success
+      ? {
+          default: catchResult.data,
+        }
+      : undefined;
+
+  return enhanceWithMetadata(schemaObject, {
+    ...maybeDefaultValue,
+  });
+};
diff --git a/src/create/schema/parsers/object.ts b/src/create/schema/parsers/object.ts
index 30ad5d8..13cdfed 100644
--- a/src/create/schema/parsers/object.ts
+++ b/src/create/schema/parsers/object.ts
@@ -17,7 +17,7 @@ import {
   createSchemaObject,
 } from '../../schema';
 
-import { isOptionalSchema } from './optional';
+import { isOptionalObjectKey } from './optional';
 import { flattenEffects } from './transform';
 
 export const createObjectSchema = <
@@ -184,7 +184,7 @@ export const createObjectSchemaFromShape = (
   state: SchemaState,
 ): Schema => {
   const properties = mapProperties(shape, state);
-  const required = mapRequired(shape, state);
+  const required = mapRequired(properties, shape, state);
   const additionalProperties = !isZodType(catchAll, 'ZodNever')
     ? createSchemaObject(catchAll, state, ['additional properties'])
     : undefined;
@@ -209,23 +209,57 @@ export const createObjectSchemaFromShape = (
 };
 
 export const mapRequired = (
+  properties: PropertyMap | undefined,
   shape: ZodRawShape,
   state: SchemaState,
 ): { required: string[]; effects?: Effect[] } | undefined => {
-  const { required, effects: allEffects } = Object.entries(shape).reduce<{
+  if (!properties) {
+    return undefined;
+  }
+
+  const { required, effects } = Object.entries(properties.schemas).reduce<{
     required: string[];
-    effects: Effect[][];
+    effects: Effect[];
   }>(
-    (acc, [key, zodSchema]) => {
-      state.path.push(`property: ${key}`);
-      const { optional, effects } = isOptionalSchema(zodSchema, state);
-      state.path.pop();
-      if (!optional) {
+    (acc, [key, schemaOrRef]) => {
+      const zodSchema = shape[key];
+      if (!zodSchema) {
+        throw new Error("Property somehow doesn't exist in shape");
+      }
+
+      const result = zodSchema.safeParse(undefined);
+
+      if (!result.success) {
         acc.required.push(key);
+        return acc;
       }
-      if (effects) {
-        acc.effects.push(effects);
+
+      // Defaulted to a value
+      if (result.data !== undefined) {
+        const baseEffect: Pick<Effect, 'zodType' | 'path'> = {
+          zodType: zodSchema as ZodType,
+          path: [...state.path, `property: ${key}`],
+        };
+
+        const effect: Effect =
+          schemaOrRef.type === 'ref'
+            ? {
+                ...baseEffect,
+                type: 'component',
+              }
+            : {
+                ...baseEffect,
+                type: 'schema',
+                creationType: state.type,
+              };
+
+        acc.effects.push(effect);
+
+        if (state.type === 'output') {
+          acc.required.push(key);
+        }
       }
+
       return acc;
     },
     {
@@ -234,10 +268,11 @@ export const mapRequired = (
     },
   );
 
-  return { required, effects: flattenEffects(allEffects) };
+  return { required, effects };
 };
 
 interface PropertyMap {
+  schemas: Record<string, Schema>;
   properties: NonNullable<oas31.SchemaObject['properties']>;
   effects: Array<Effect[] | undefined>;
 }
@@ -253,22 +288,19 @@ export const mapProperties = (
   }
   return shapeEntries.reduce(
     (acc, [key, zodSchema]) => {
-      if (
-        isZodType(zodSchema, 'ZodNever') ||
-        isZodType(zodSchema, 'ZodUndefined')
-      ) {
+      if (isOptionalObjectKey(zodSchema)) {
         return acc;
       }
 
-      const property = createSchemaObject(zodSchema, state, [
-        `property: ${key}`,
-      ]);
+      const schema = createSchemaObject(zodSchema, state, [`property: ${key}`]);
 
-      acc.properties[key] = property.schema;
-      acc.effects.push(property.effects);
+      acc.schemas[key] = schema;
+      acc.properties[key] = schema.schema;
+      acc.effects.push(schema.effects);
       return acc;
     },
     {
+      schemas: {},
       properties: {},
       effects: [],
     } as PropertyMap,
diff --git a/src/create/schema/parsers/optional.ts b/src/create/schema/parsers/optional.ts
index 57c76b1..831c844 100644
--- a/src/create/schema/parsers/optional.ts
+++ b/src/create/schema/parsers/optional.ts
@@ -1,114 +1,18 @@
-import type { ZodOptional, ZodType, ZodTypeAny } from 'zod';
+import type { ZodOptional, ZodTypeAny } from 'zod';
 
 import { isZodType } from '../../../zodType';
-import type { Effect } from '../../components';
 import {
   type Schema,
   type SchemaState,
   createSchemaObject,
 } from '../../schema';
 
-import { flattenEffects } from './transform';
-
 export const createOptionalSchema = <T extends ZodTypeAny>(
   zodOptional: ZodOptional<T>,
   state: SchemaState,
 ): Schema => createSchemaObject(zodOptional.unwrap(), state, ['optional']); // Optional doesn't change OpenAPI schema
 
-type OptionalResult = { optional: boolean; effects?: Effect[] };
-
-export const isOptionalSchema = (
-  zodSchema: ZodTypeAny,
-  state: SchemaState,
-): OptionalResult => {
-  if (
-    isZodType(zodSchema, 'ZodOptional') ||
-    isZodType(zodSchema, 'ZodNever') ||
-    isZodType(zodSchema, 'ZodUndefined') ||
-    (isZodType(zodSchema, 'ZodLiteral') && zodSchema._def.value === undefined)
-  ) {
-    return { optional: true };
-  }
-
-  if (isZodType(zodSchema, 'ZodDefault')) {
-    if (zodSchema._def.openapi?.effectType === 'input') {
-      return { optional: true };
-    }
-
-    if (zodSchema._def.openapi?.effectType === 'output') {
-      return { optional: false };
-    }
-
-    return {
-      optional: state.type === 'input',
-      effects: [
-        {
-          type: 'schema',
-          creationType: state.type,
-          zodType: zodSchema,
-          path: [...state.path],
-        },
-      ],
-    };
-  }
-
-  if (isZodType(zodSchema, 'ZodNullable') || isZodType(zodSchema, 'ZodCatch')) {
-    return isOptionalSchema(zodSchema._def.innerType, state);
-  }
-
-  if (isZodType(zodSchema, 'ZodEffects')) {
-    // zod custom types
-    if (isZodType(zodSchema._def.schema, 'ZodAny')) {
-      return { optional: zodSchema.isOptional() };
-    }
-
-    return isOptionalSchema(zodSchema._def.schema, state);
-  }
-
-  if (
-    isZodType(zodSchema, 'ZodUnion') ||
-    isZodType(zodSchema, 'ZodDiscriminatedUnion')
-  ) {
-    const results = (zodSchema._def.options as ZodTypeAny[]).map((schema) =>
-      isOptionalSchema(schema, state),
-    );
-    return results.reduce(
-      (acc, result) => ({
-        optional: acc.optional || result.optional,
-        effects: flattenEffects([acc.effects, result.effects]),
-      }),
-      { optional: false },
-    );
-  }
-
-  if (isZodType(zodSchema, 'ZodIntersection')) {
-    const results = [zodSchema._def.left, zodSchema._def.right].map((schema) =>
-      isOptionalSchema(schema, state),
-    );
-    return results.reduce(
-      (acc, result) => ({
-        optional: acc.optional || result.optional,
-        effects: flattenEffects([acc.effects, result.effects]),
-      }),
-      { optional: false },
-    );
-  }
-
-  if (isZodType(zodSchema, 'ZodPipeline')) {
-    const type = zodSchema._def.openapi?.effectType ?? state.type;
-
-    if (type === 'input') {
-      return isOptionalSchema(zodSchema._def.in, state);
-    }
-
-    if (type === 'output') {
-      return isOptionalSchema(zodSchema._def.out, state);
-    }
-  }
-
-  if (isZodType(zodSchema, 'ZodLazy')) {
-    return isOptionalSchema(zodSchema._def.getter() as ZodType, state);
-  }
-
-  return { optional: zodSchema.isOptional() };
-};
+export const isOptionalObjectKey = (zodSchema: ZodTypeAny) =>
+  isZodType(zodSchema, 'ZodNever') ||
+  isZodType(zodSchema, 'ZodUndefined') ||
+  (isZodType(zodSchema, 'ZodLiteral') && zodSchema._def.value === undefined);
diff --git a/src/create/schema/parsers/union.ts b/src/create/schema/parsers/union.ts
index af92a4c..2b57d7c 100644
--- a/src/create/schema/parsers/union.ts
+++ b/src/create/schema/parsers/union.ts
@@ -6,6 +6,7 @@ import {
   createSchemaObject,
 } from '../../schema';
 
+import { isOptionalObjectKey } from './optional';
 import { flattenEffects } from './transform';
 
 export const createUnionSchema = <
@@ -14,9 +15,12 @@ export const createUnionSchema = <
   zodUnion: ZodUnion<T>,
   state: SchemaState,
 ): Schema => {
-  const schemas = zodUnion.options.map((option, index) =>
-    createSchemaObject(option, state, [`union option ${index}`]),
-  );
+  const schemas = zodUnion.options.reduce<Schema[]>((acc, option, index) => {
+    if (!isOptionalObjectKey(option)) {
+      acc.push(createSchemaObject(option, state, [`union option ${index}`]));
+    }
+    return acc;
+  }, []);
 
   if (zodUnion._def.openapi?.unionOneOf ?? state.documentOptions?.unionOneOf) {
     return {


Test patch:
diff --git a/src/create/schema/index.test.ts b/src/create/schema/index.test.ts
index f47b1ce..7dcd7f4 100644
--- a/src/create/schema/index.test.ts
+++ b/src/create/schema/index.test.ts
@@ -191,6 +191,7 @@ const expectedZodUnion: Schema['schema'] = {
 const zodCatch = z.string().catch('bob');
 const expectedZodCatch: Schema['schema'] = {
   type: 'string',
+  default: 'bob',
 };
 
 const zodPipeline = z
diff --git a/src/create/schema/parsers/catch.test.ts b/src/create/schema/parsers/catch.test.ts
index 0b4a93b..3aff702 100644
--- a/src/create/schema/parsers/catch.test.ts
+++ b/src/create/schema/parsers/catch.test.ts
@@ -7,11 +7,12 @@ import { createOutputState } from '../../../testing/state';
 import { createCatchSchema } from './catch';
 
 describe('createCatchSchema', () => {
-  it('creates a simple string schema for a string with a catch', () => {
+  it('creates a default string schema for a string with a catch', () => {
     const expected: Schema = {
       type: 'schema',
       schema: {
         type: 'string',
+        default: 'bob',
       },
     };
     const schema = z.string().catch('bob');
diff --git a/src/create/schema/parsers/discriminatedUnion.test.ts b/src/create/schema/parsers/discriminatedUnion.test.ts
index 3ea6cb4..7c77470 100644
--- a/src/create/schema/parsers/discriminatedUnion.test.ts
+++ b/src/create/schema/parsers/discriminatedUnion.test.ts
@@ -443,7 +443,22 @@ describe('createDiscriminatedUnionSchema', () => {
   });
 
   it('handles a discriminated union with a catch type', () => {
-    const expected: Schema = {
+    const schema = z.discriminatedUnion('type', [
+      z
+        .object({
+          type: z.literal('a').catch('a'),
+        })
+        .openapi({ ref: 'a' }),
+      z
+        .object({
+          type: z.literal('b'),
+        })
+        .openapi({ ref: 'b' }),
+    ]);
+
+    const result = createDiscriminatedUnionSchema(schema, createOutputState());
+
+    expect(result).toEqual<Schema>({
       type: 'schema',
       schema: {
         discriminator: {
@@ -462,22 +477,13 @@ describe('createDiscriminatedUnionSchema', () => {
           },
         ],
       },
-    };
-    const schema = z.discriminatedUnion('type', [
-      z
-        .object({
-          type: z.literal('a').catch('a'),
-        })
-        .openapi({ ref: 'a' }),
-      z
-        .object({
-          type: z.literal('b'),
-        })
-        .openapi({ ref: 'b' }),
-    ]);
-
-    const result = createDiscriminatedUnionSchema(schema, createOutputState());
-
-    expect(result).toEqual(expected);
+      effects: [
+        {
+          type: 'component',
+          path: ['discriminated union option 0'],
+          zodType: schema.options[0],
+        },
+      ],
+    });
   });
 });
diff --git a/src/create/schema/parsers/object.test.ts b/src/create/schema/parsers/object.test.ts
index 496a3f9..a526f40 100644
--- a/src/create/schema/parsers/object.test.ts
+++ b/src/create/schema/parsers/object.test.ts
@@ -101,6 +101,34 @@ describe('createObjectSchema', () => {
     expect(result).toEqual(expected);
   });
 
+  it('considers ZodCatch in an input state as an effect', () => {
+    const schema = z.object({
+      a: z.string().catch('a'),
+    });
+
+    const expected: Schema = {
+      type: 'schema',
+      schema: {
+        type: 'object',
+        properties: {
+          a: { type: 'string', default: 'a' },
+        },
+      },
+      effects: [
+        {
+          type: 'schema',
+          creationType: 'input',
+          zodType: schema.shape.a,
+          path: ['property: a'],
+        },
+      ],
+    };
+
+    const result = createObjectSchema(schema, createInputState());
+
+    expect(result).toEqual(expected);
+  });
+
   it('considers ZodDefault in an output state as an effect', () => {
     const schema = z.object({
       a: z.string().default('a'),
@@ -129,6 +157,194 @@ describe('createObjectSchema', () => {
 
     expect(result).toEqual(expected);
   });
+
+  it('considers ZodCatch in an output state as an effect', () => {
+    const schema = z.object({
+      a: z.string().catch('a'),
+    });
+
+    const expected: Schema = {
+      type: 'schema',
+      schema: {
+        type: 'object',
+        properties: {
+          a: { type: 'string', default: 'a' },
+        },
+        required: ['a'],
+      },
+      effects: [
+        {
+          type: 'schema',
+          creationType: 'output',
+          zodType: schema.shape.a,
+          path: ['property: a'],
+        },
+      ],
+    };
+
+    const result = createObjectSchema(schema, createOutputState());
+
+    expect(result).toEqual(expected);
+  });
+});
+
+describe('required', () => {
+  describe('output', () => {
+    it('creates a required array containing all properties', () => {
+      const schema = z.object({
+        a: z.string(),
+        b: z.string().nullable(),
+        c: z.number(),
+        d: z.literal(null),
+        e: z.union([z.string(), z.number()]),
+        f: z.custom((r) => r !== undefined),
+        g: z.string().default('a'),
+        h: z.string().catch('a'),
+      });
+
+      const result = createObjectSchema(schema, createOutputState());
+
+      expect(result).toEqual<Schema>({
+        effects: expect.any(Array),
+        type: 'schema',
+        schema: {
+          type: 'object',
+          properties: {
+            a: { type: 'string' },
+            b: { type: ['string', 'null'] },
+            c: { type: 'number' },
+            d: { type: 'null' },
+            e: { anyOf: [{ type: 'string' }, { type: 'number' }] },
+            f: {},
+            g: { type: 'string', default: 'a' },
+            h: { type: 'string', default: 'a' },
+          },
+          required: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
+        },
+      });
+    });
+
+    it('does not create an required array', () => {
+      const ref = z.string().openapi({ ref: 'ref' });
+      const oref = z.string().optional().openapi({ ref: 'oref' });
+      const schema = z.object({
+        a: z.literal(undefined),
+        b: z.never(),
+        c: z.undefined(),
+        d: z.string().optional(),
+        e: z.string().nullish(),
+        f: z.number().optional(),
+        g: z.union([z.string(), z.undefined()]),
+        h: z.union([z.string(), z.number().optional()]),
+        i: ref.optional(),
+        j: oref,
+        k: z.custom<string | undefined>(),
+        l: z
+          .string()
+          .optional()
+          .transform((str) => str?.length)
+          .pipe(z.number().optional()),
+      });
+
+      const result = createObjectSchema(schema, createOutputState());
+
+      expect(result).toEqual<Schema>({
+        effects: expect.any(Array),
+        type: 'schema',
+        schema: {
+          type: 'object',
+          properties: {
+            d: { type: 'string' },
+            e: { type: ['string', 'null'] },
+            f: { type: 'number' },
+            g: { anyOf: [{ type: 'string' }] },
+            h: { anyOf: [{ type: 'string' }, { type: 'number' }] },
+            i: { $ref: '#/components/schemas/ref' },
+            j: { $ref: '#/components/schemas/oref' },
+            k: {},
+            l: { type: 'number' },
+          },
+        },
+      });
+    });
+  });
+
+  describe('input', () => {
+    it('creates a required array containing all properties', () => {
+      const schema = z.object({
+        a: z.string(),
+        b: z.string().nullable(),
+        c: z.number(),
+        d: z.literal(null),
+        e: z.union([z.string(), z.number()]),
+        f: z.custom((r) => r !== undefined),
+      });
+
+      const result = createObjectSchema(schema, createInputState());
+
+      expect(result).toEqual<Schema>({
+        type: 'schema',
+        schema: {
+          type: 'object',
+          properties: {
+            a: { type: 'string' },
+            b: { type: ['string', 'null'] },
+            c: { type: 'number' },
+            d: { type: 'null' },
+            e: { anyOf: [{ type: 'string' }, { type: 'number' }] },
+            f: {},
+          },
+          required: ['a', 'b', 'c', 'd', 'e', 'f'],
+        },
+      });
+    });
+
+    it('does not create an required array', () => {
+      const ref = z.string().openapi({ ref: 'ref' });
+      const oref = z.string().optional().openapi({ ref: 'oref' });
+      const schema = z.object({
+        a: z.literal(undefined),
+        b: z.never(),
+        c: z.undefined(),
+        d: z.string().optional(),
+        e: z.string().nullish(),
+        f: z.number().optional(),
+        g: z.union([z.string(), z.undefined()]),
+        h: z.union([z.string(), z.number().optional()]),
+        i: ref.optional(),
+        j: oref,
+        k: z.custom<string | undefined>(),
+        l: z
+          .string()
+          .optional()
+          .transform((str) => str?.length)
+          .pipe(z.number().optional()),
+        m: z.string().default('a'),
+      });
+
+      const result = createObjectSchema(schema, createInputState());
+
+      expect(result).toEqual<Schema>({
+        effects: expect.any(Array),
+        type: 'schema',
+        schema: {
+          type: 'object',
+          properties: {
+            d: { type: 'string' },
+            e: { type: ['string', 'null'] },
+            f: { type: 'number' },
+            g: { anyOf: [{ type: 'string' }] },
+            h: { anyOf: [{ type: 'string' }, { type: 'number' }] },
+            i: { $ref: '#/components/schemas/ref' },
+            j: { $ref: '#/components/schemas/oref' },
+            k: {},
+            l: { type: 'string' },
+            m: { type: 'string', default: 'a' },
+          },
+        },
+      });
+    });
+  });
 });
 
 describe('extend', () => {
@@ -270,24 +486,6 @@ describe('extend', () => {
     expect(result).toEqual(expected);
   });
 
-  it('ignores ZodNever and ZodUndefined schemas', () => {
-    const expected: Schema = {
-      type: 'schema',
-      schema: {
-        type: 'object',
-        properties: {
-          a: { type: 'string' },
-        },
-        required: ['a'],
-      },
-    };
-    const schema = z.object({ a: z.string(), b: z.undefined(), c: z.never() });
-
-    const result = createObjectSchema(schema, createOutputState());
-
-    expect(result).toEqual(expected);
-  });
-
   it('creates an object with 2 required fields using a custom type', () => {
     const expected: Schema = {
       type: 'schema',
diff --git a/src/create/schema/parsers/optional.test.ts b/src/create/schema/parsers/optional.test.ts
index c761503..4c126cb 100644
--- a/src/create/schema/parsers/optional.test.ts
+++ b/src/create/schema/parsers/optional.test.ts
@@ -2,9 +2,9 @@ import '../../../entries/extend';
 import { z } from 'zod';
 
 import type { Schema } from '..';
-import { createInputState, createOutputState } from '../../../testing/state';
+import { createOutputState } from '../../../testing/state';
 
-import { createOptionalSchema, isOptionalSchema } from './optional';
+import { createOptionalSchema } from './optional';
 
 describe('createOptionalSchema', () => {
   it('creates a simple string schema for an optional string', () => {
@@ -21,256 +21,3 @@ describe('createOptionalSchema', () => {
     expect(result).toEqual(expected);
   });
 });
-
-describe('isOptionalSchema', () => {
-  it('returns true for an optional string', () => {
-    const schema = z.string().optional();
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for an optional transform', () => {
-    const schema = z
-      .string()
-      .optional()
-      .transform((str) => str?.length ?? 0);
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for a union with an optional', () => {
-    const schema = z.union([z.string(), z.string().optional()]);
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for an intersection with an optional', () => {
-    const schema = z.intersection(z.string(), z.string().optional());
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for a nullable with an optional', () => {
-    const schema = z.string().optional().nullable();
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for an async transform with an optional', () => {
-    const schema = z
-      .string()
-      .optional()
-      // eslint-disable-next-line @typescript-eslint/require-await
-      .transform(async () => 'x');
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns false for an async transform without an optional', () => {
-    const schema = z
-      .string()
-      // eslint-disable-next-line @typescript-eslint/require-await
-      .transform(async () => 'x');
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: false });
-  });
-
-  it('returns false for a custom without an optional', () => {
-    const schema = z.custom<Date>((d) => d instanceof Date);
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: false });
-  });
-
-  it('returns true for a custom undefined without a check', () => {
-    const schema = z.custom<undefined>();
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns false for a custom string | undefined without a check', () => {
-    const schema = z.custom<string | undefined>();
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns false for a custom string | undefined with a check that returns false for undefined', () => {
-    const schema = z.custom<string | undefined>(
-      (toCheck: string | undefined) => toCheck !== undefined,
-    );
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: false });
-  });
-
-  it('returns true for a custom string | undefined with a check that returns true for undefined', () => {
-    const schema = z.custom<string | undefined>(
-      (toCheck: string | undefined) => toCheck === undefined,
-    );
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for a custom undefined with a check', () => {
-    const schema = z.custom<undefined>((_: unknown) => false);
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: false });
-  });
-
-  it('returns true for a custom with Date with an optional', () => {
-    const schema = z.custom<Date>((d) => d instanceof Date).optional();
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns false for a custom without an optional with async transform', () => {
-    const schema = z
-      .custom<Date>((d) => d instanceof Date)
-      // eslint-disable-next-line @typescript-eslint/require-await
-      .transform(async () => 'x');
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: false });
-  });
-
-  it('returns true for a custom with an optional using a transform', () => {
-    const schema = z
-      .custom<Date>((d) => d instanceof Date)
-      .optional()
-      // eslint-disable-next-line @typescript-eslint/require-await
-      .transform(async () => 'x');
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for an output state on pipeline', () => {
-    const schema = z
-      .string()
-      .transform((str) => str.length)
-      .pipe(z.number().optional());
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for an input state on pipeline', () => {
-    const schema = z
-      .string()
-      .optional()
-      .transform((str) => str?.length ?? 0)
-      .pipe(z.number());
-
-    const result = isOptionalSchema(schema, createInputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for a zod default', () => {
-    const schema = z.string().default('a');
-
-    const result = isOptionalSchema(schema, createInputState());
-
-    expect(result).toEqual({
-      optional: true,
-      effects: [
-        {
-          type: 'schema',
-          creationType: 'input',
-          zodType: schema,
-          path: [],
-        },
-      ],
-    });
-  });
-
-  it('returns true for a zod default in output state with effectType input', () => {
-    const schema = z.string().default('a').openapi({ effectType: 'input' });
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({
-      optional: true,
-    });
-  });
-
-  it('returns false for a zod default in input state with effectType output', () => {
-    const schema = z.string().default('a').openapi({ effectType: 'output' });
-
-    const result = isOptionalSchema(schema, createInputState());
-
-    expect(result).toEqual({
-      optional: false,
-    });
-  });
-
-  it('returns true for an input effectType on an output state pipeline', () => {
-    const schema = z
-      .string()
-      .optional()
-      .transform((str) => str?.length ?? 0)
-      .pipe(z.number())
-      .openapi({ effectType: 'input' });
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns false for an output effectType on an input state pipeline', () => {
-    const schema = z
-      .string()
-      .optional()
-      .transform((str) => str?.length ?? 0)
-      .pipe(z.number())
-      .openapi({ effectType: 'output' });
-
-    const result = isOptionalSchema(schema, createInputState());
-
-    expect(result).toEqual({ optional: false });
-  });
-
-  it('returns true for zod undefined', () => {
-    const schema = z.undefined();
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for zod never', () => {
-    const schema = z.never();
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-
-  it('returns true for zod literal undefined', () => {
-    const schema = z.literal(undefined);
-
-    const result = isOptionalSchema(schema, createOutputState());
-
-    expect(result).toEqual({ optional: true });
-  });
-});
diff --git a/src/create/schema/parsers/union.test.ts b/src/create/schema/parsers/union.test.ts
index a7ba166..600d24d 100644
--- a/src/create/schema/parsers/union.test.ts
+++ b/src/create/schema/parsers/union.test.ts
@@ -100,4 +100,26 @@ describe('createUnionSchema', () => {
 
     expect(result).toEqual(expected);
   });
+
+  it('ignores optional values in a union', () => {
+    const schema = z.union([
+      z.string(),
+      z.literal(undefined),
+      z.undefined(),
+      z.never(),
+    ]);
+
+    const result = createUnionSchema(schema, createOutputState());
+
+    expect(result).toEqual<Schema>({
+      type: 'schema',
+      schema: {
+        anyOf: [
+          {
+            type: 'string',
+          },
+        ],
+      },
+    });
+  });
 });