Repository: samchungy/zod-openapi
Pull request #356 (2024-11-05 03:38:48)

`.openapi()` Breaking Change
Resolves https://github.com/samchungy/zod-openapi/issues/303

`.openapi()` behaves a little differently now.

It now will attempt to store any `previous` openapi state and use it.

## Why do we need this change?

1. Unintuitive Extension
	```ts
	const FooSchema = z.string().openapi({ ref: 'string' });
	
	const BarSchema = z.object({
	  a: FooSchema,
	  b: FooSchema.openapi({ description: 'foo' }),
	});
	
	createSchema(BarSchema)
	```
	
	This currently generates the following:
	
	```ts
	{
	  schema: {
	    type: 'object',
	    properties: {
	      a: { '$ref': '#/components/schemas/string' },
	      b: { type: 'string', description: 'foo' }
	    },
	    required: [ 'a', 'b' ]
	  },
	  components: { string: { type: 'string' } }
	}
	```

	However, it's clear that the user simply wanted to reuse the FooSchema but describe it as something else later on.
	
	After:
	```ts
	{
	  schema: {
	    type: 'object',
	    properties: {
	      a: { '$ref': '#/components/schemas/string' },
	      b: { '$ref': '#/components/schemas/string', description: 'foo' }
	    },
	    required: [ 'a', 'b' ]
	  },
	  components: { string: { type: 'string' } }
	}
	```

2. Broken `.describe()` behaviour on registered schemas

	```ts
		const FooSchema = z.string().openapi({ ref: 'string' });
		
		const BarSchema = z.object({
		  a: FooSchema,
		  b: FooSchema.describe('foo')
		});
		
		createSchema(BarSchema)
	```
	
	This scenario actually causes us to crash because it attempts to register the FooSchema twice with different objects because the `.describe()` triggers `Schema "string" is already registered`
	
	After:
	```ts
	{
	  schema: {
	    type: 'object',
	    properties: {
	      a: { '$ref': '#/components/schemas/string' },
	      b: { '$ref': '#/components/schemas/string', description: 'foo' }
	    },
	    required: [ 'a', 'b' ]
	  },
	  components: { string: { type: 'string' } }
	}
	```

3. Unregistered Base Schemas. You would think that if you created a base schema, and used it in a schema with something like a description, we would register it? Right now? No.

	```ts
	const FooSchema = z.string().openapi({ ref: 'string' });
	const BarSchema = z.object({
	  a: FooSchema.describe('foo')
	});
	createSchema(BarSchema)
	```
	
	```ts
	{
	  schema: {
	    type: 'object',
	    properties: { b: { type: 'string', description: 'foo' } },
	    required: [ 'b' ]
	  },
	}
	```
	
	After:

	```ts
	{
	  schema: {
	    type: 'object',
	    properties: {
	      a: { '$ref': '#/components/schemas/string', description: 'foo' }
	    },
	    required: [ 'a', 'b' ]
	  },
	  components: { string: { type: 'string' } }
	}
	```


samchungy: TODO: Figure out how we're going to calculate previous state
samchungy: TODO: Add much more tests

Patch:
diff --git a/src/create/components.ts b/src/create/components.ts
index 91306bc..5bae28e 100644
--- a/src/create/components.ts
+++ b/src/create/components.ts
@@ -254,7 +254,7 @@ const getSchemas = (
           `Schema ${JSON.stringify(schema._def)} is already registered`,
         );
       }
-      const ref = schema._def.openapi?.ref ?? key;
+      const ref = schema._def.zodOpenApi?.openapi?.ref ?? key;
       components.schemas.set(schema, {
         type: 'manual',
         ref,
@@ -278,9 +278,9 @@ const getParameters = (
           `Parameter ${JSON.stringify(schema._def)} is already registered`,
         );
       }
-      const ref = schema._def.openapi?.param?.ref ?? key;
-      const name = schema._def.openapi?.param?.name;
-      const location = schema._def.openapi?.param?.in;
+      const ref = schema._def.zodOpenApi?.openapi?.param?.ref ?? key;
+      const name = schema._def.zodOpenApi?.openapi?.param?.name;
+      const location = schema._def.zodOpenApi?.openapi?.param?.in;
 
       if (!name || !location) {
         throw new Error('`name` or `in` missing in .openapi()');
@@ -310,7 +310,7 @@ const getHeaders = (
           `Header ${JSON.stringify(schema._def)} is already registered`,
         );
       }
-      const ref = schema._def.openapi?.param?.ref ?? key;
+      const ref = schema._def.zodOpenApi?.openapi?.param?.ref ?? key;
       components.headers.set(schema, {
         type: 'manual',
         ref,
@@ -456,7 +456,7 @@ export const createSchemaComponents = (
     if (type === 'manual') {
       const state: SchemaState = {
         components,
-        type: schema._def.openapi?.refType ?? 'output',
+        type: schema._def.zodOpenApi?.openapi?.refType ?? 'output',
         path: [],
         visited: new Set(),
         documentOptions,
diff --git a/src/create/parameters.ts b/src/create/parameters.ts
index 32347e2..c491b62 100644
--- a/src/create/parameters.ts
+++ b/src/create/parameters.ts
@@ -20,7 +20,7 @@ export const createBaseParameter = (
   subpath: string[],
   documentOptions?: CreateDocumentOptions,
 ): oas31.BaseParameterObject => {
-  const { ref, ...rest } = schema._def.openapi?.param ?? {};
+  const { ref, ...rest } = schema._def.zodOpenApi?.openapi?.param ?? {};
   const state: SchemaState = {
     components,
     type: 'input',
@@ -32,7 +32,7 @@ export const createBaseParameter = (
   const required = !schema.isOptional();
 
   const description =
-    schema._def.openapi?.description ?? schema._def.description;
+    schema._def.zodOpenApi?.openapi?.description ?? schema._def.description;
 
   return {
     ...(description && { description }),
@@ -51,9 +51,10 @@ export const createParamOrRef = (
   documentOptions?: CreateDocumentOptions,
 ): oas31.ParameterObject | oas31.ReferenceObject => {
   const component = components.parameters.get(zodSchema);
-  const paramType = zodSchema._def?.openapi?.param?.in ?? component?.in ?? type;
+  const paramType =
+    zodSchema._def.zodOpenApi?.openapi?.param?.in ?? component?.in ?? type;
   const paramName =
-    zodSchema._def?.openapi?.param?.name ?? component?.name ?? name;
+    zodSchema._def.zodOpenApi?.openapi?.param?.name ?? component?.name ?? name;
 
   if (!paramType) {
     throw new Error('Parameter type missing');
@@ -86,7 +87,7 @@ export const createParamOrRef = (
     throw new Error('Unexpected Error: received a reference object');
   }
 
-  const ref = zodSchema?._def?.openapi?.param?.ref ?? component?.ref;
+  const ref = zodSchema?._def.zodOpenApi?.openapi?.param?.ref ?? component?.ref;
 
   const paramObject: oas31.ParameterObject = {
     in: paramType,
diff --git a/src/create/responses.ts b/src/create/responses.ts
index 4d487b6..3207a55 100644
--- a/src/create/responses.ts
+++ b/src/create/responses.ts
@@ -59,7 +59,7 @@ export const createHeaderOrRef = (
     throw new Error('Unexpected Error: received a reference object');
   }
 
-  const ref = schema._def?.openapi?.header?.ref ?? component?.ref;
+  const ref = schema._def.zodOpenApi?.openapi?.header?.ref ?? component?.ref;
 
   if (ref) {
     components.headers.set(schema, {
@@ -80,7 +80,7 @@ export const createBaseHeader = (
   components: ComponentsObject,
   documentOptions?: CreateDocumentOptions,
 ): oas31.BaseParameterObject => {
-  const { ref, ...rest } = schema._def.openapi?.header ?? {};
+  const { ref, ...rest } = schema._def.zodOpenApi?.openapi?.header ?? {};
   const state: SchemaState = {
     components,
     type: 'output',
diff --git a/src/create/schema/index.ts b/src/create/schema/index.ts
index e9abb7f..7a00d3a 100644
--- a/src/create/schema/index.ts
+++ b/src/create/schema/index.ts
@@ -1,6 +1,6 @@
 import type { ZodType, ZodTypeDef } from 'zod';
 
-import type { oas31 } from '../../openapi3-ts/dist';
+import type { oas30, oas31 } from '../../openapi3-ts/dist';
 import {
   type ComponentsObject,
   type CreationType,
@@ -24,17 +24,19 @@ export interface SchemaState {
   documentOptions?: CreateSchemaOptions;
 }
 
-const isDescriptionEqual = (schema: Schema, zodSchema: ZodType): boolean =>
-  schema.type === 'ref' && zodSchema.description === schema.zodType.description;
-
 export const createNewSchema = <
   Output = unknown,
   Def extends ZodTypeDef = ZodTypeDef,
   Input = Output,
->(
-  zodSchema: ZodType<Output, Def, Input>,
-  state: SchemaState,
-): Schema => {
+>({
+  zodSchema,
+  previous,
+  state,
+}: {
+  zodSchema: ZodType<Output, Def, Input>;
+  previous: RefObject | undefined;
+  state: SchemaState;
+}): Schema => {
   if (state.visited.has(zodSchema)) {
     throw new Error(
       `The schema at ${state.path.join(
@@ -51,18 +53,16 @@ export const createNewSchema = <
     refType,
     unionOneOf,
     ...additionalMetadata
-  } = zodSchema._def.openapi ?? {};
+  } = zodSchema._def.zodOpenApi?.openapi ?? {};
 
-  const schema = createSchemaSwitch(zodSchema, state);
-  const description =
-    zodSchema.description && !isDescriptionEqual(schema, zodSchema)
-      ? zodSchema.description
-      : undefined;
+  const schema = createSchemaSwitch(zodSchema, previous, state);
 
-  const schemaWithMetadata = enhanceWithMetadata(schema, {
-    ...(description && { description }),
-    ...additionalMetadata,
-  });
+  const schemaWithMetadata = enhanceWithMetadata(
+    schema,
+    additionalMetadata,
+    state,
+    previous,
+  );
   state.visited.delete(zodSchema);
   return schemaWithMetadata;
 };
@@ -71,19 +71,29 @@ export const createNewRef = <
   Output = unknown,
   Def extends ZodTypeDef = ZodTypeDef,
   Input = Output,
->(
-  ref: string,
-  zodSchema: ZodType<Output, Def, Input>,
-  state: SchemaState,
-): Schema => {
+>({
+  previous,
+  ref,
+  zodSchema,
+  state,
+}: {
+  ref: string;
+  zodSchema: ZodType<Output, Def, Input>;
+  previous: RefObject | undefined;
+  state: SchemaState;
+}): Schema => {
   state.components.schemas.set(zodSchema, {
     type: 'in-progress',
     ref,
   });
 
-  const newSchema = createNewSchema(zodSchema, {
-    ...state,
-    visited: new Set(),
+  const newSchema = createNewSchema({
+    zodSchema,
+    previous,
+    state: {
+      ...state,
+      visited: new Set(),
+    },
   });
 
   state.components.schemas.set(zodSchema, {
@@ -101,6 +111,7 @@ export const createNewRef = <
         state.documentOptions?.componentRefPath,
       ),
     },
+    schemaObject: newSchema.schema,
     effects: newSchema.effects
       ? [
           {
@@ -122,7 +133,7 @@ export const createExistingRef = <
   zodSchema: ZodType<Output, Def, Input>,
   component: SchemaComponent | undefined,
   state: SchemaState,
-): Schema | undefined => {
+): RefObject | undefined => {
   if (component && component.type === 'complete') {
     return {
       type: 'ref',
@@ -132,6 +143,7 @@ export const createExistingRef = <
           state.documentOptions?.componentRefPath,
         ),
       },
+      schemaObject: component.schemaObject,
       effects: component.effects
         ? [
             {
@@ -154,6 +166,7 @@ export const createExistingRef = <
           state.documentOptions?.componentRefPath,
         ),
       },
+      schemaObject: undefined,
       effects: [
         {
           type: 'component',
@@ -175,6 +188,12 @@ export type BaseObject = {
 export type RefObject = BaseObject & {
   type: 'ref';
   schema: oas31.ReferenceObject;
+  schemaObject:
+    | oas31.SchemaObject
+    | oas31.ReferenceObject
+    | oas30.ReferenceObject
+    | oas30.SchemaObject
+    | undefined;
   zodType: ZodType;
 };
 
@@ -192,7 +211,8 @@ export const createSchemaOrRef = <
 >(
   zodSchema: ZodType<Output, Def, Input>,
   state: SchemaState,
-): Schema => {
+  onlyRef?: boolean,
+): Schema | undefined => {
   const component = state.components.schemas.get(zodSchema);
   const existingRef = createExistingRef(zodSchema, component, state);
 
@@ -200,12 +220,32 @@ export const createSchemaOrRef = <
     return existingRef;
   }
 
-  const ref = zodSchema._def.openapi?.ref ?? component?.ref;
+  const previous = zodSchema._def.zodOpenApi?.previous
+    ? (createSchemaOrRef(zodSchema._def.zodOpenApi.previous, state, true) as
+        | RefObject
+        | undefined)
+    : undefined;
+
+  const current =
+    zodSchema._def.zodOpenApi?.current &&
+    zodSchema._def.zodOpenApi.current !== zodSchema
+      ? (createSchemaOrRef(zodSchema._def.zodOpenApi.current, state, true) as
+          | RefObject
+          | undefined)
+      : undefined;
+
+  const ref = zodSchema._def.zodOpenApi?.openapi?.ref ?? component?.ref;
   if (ref) {
-    return createNewRef(ref, zodSchema, state);
+    return current
+      ? createNewSchema({ zodSchema, previous: current, state })
+      : createNewRef({ ref, zodSchema, previous, state });
+  }
+
+  if (onlyRef) {
+    return previous ?? current;
   }
 
-  return createNewSchema(zodSchema, state);
+  return createNewSchema({ zodSchema, previous: previous ?? current, state });
 };
 
 export const createSchemaObject = <
@@ -219,6 +259,10 @@ export const createSchemaObject = <
 ): Schema => {
   state.path.push(...subpath);
   const schema = createSchemaOrRef(zodSchema, state);
+
+  if (!schema) {
+    throw new Error('Schema does not exist');
+  }
   state.path.pop();
   return schema;
 };
diff --git a/src/create/schema/metadata.ts b/src/create/schema/metadata.ts
index 0fe3241..fb941fe 100644
--- a/src/create/schema/metadata.ts
+++ b/src/create/schema/metadata.ts
@@ -1,25 +1,160 @@
+import { satisfiesVersion } from '../../openapi';
 import type { oas31 } from '../../openapi3-ts/dist';
 
-import type { Schema } from '.';
+import type { RefObject, Schema, SchemaState } from '.';
+
+export const createDescriptionMetadata = (
+  schema: RefObject,
+  description: string,
+  state: SchemaState,
+): Schema => {
+  if (satisfiesVersion(state.components.openapi, '3.1.0')) {
+    return {
+      type: 'ref',
+      schema: {
+        $ref: schema.schema.$ref,
+        description,
+      },
+      zodType: schema.zodType,
+      effects: schema.effects,
+      schemaObject: schema.schemaObject,
+    };
+  }
+
+  return {
+    type: 'schema',
+    schema: {
+      description,
+      allOf: [schema.schema],
+    },
+    effects: schema.effects,
+  };
+};
+
+const isValueEqual = (value: unknown, previous: unknown): boolean => {
+  if (typeof value !== typeof previous) {
+    return false;
+  }
+
+  if (
+    typeof value === 'string' ||
+    typeof value === 'number' ||
+    typeof value === 'boolean'
+  ) {
+    return value === previous;
+  }
+
+  if (Array.isArray(value) && Array.isArray(previous)) {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const sorted = [...value].sort();
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const previousSorted = [...previous].sort();
+
+    return sorted.every((v, i) => isValueEqual(v, previousSorted[i]));
+  }
+
+  if (value === null || previous === null) {
+    return value === previous;
+  }
+
+  if (typeof value === 'object' && typeof previous === 'object') {
+    const keys = Object.keys(value);
+
+    return keys.every((key) =>
+      isValueEqual(
+        (value as Record<string, unknown>)[key],
+        (previous as Record<string, unknown>)[key],
+      ),
+    );
+  }
+
+  return value === previous;
+};
 
 export const enhanceWithMetadata = (
   schema: Schema,
   metadata: oas31.SchemaObject | oas31.ReferenceObject,
+  state: SchemaState,
+  previous: RefObject | undefined,
 ): Schema => {
+  const values = Object.entries(metadata).reduce(
+    (acc, [key, value]) => {
+      if (value === undefined) {
+        return acc;
+      }
+
+      acc[key] = value;
+      return acc;
+    },
+    {} as Record<string, unknown>,
+  ) as oas31.SchemaObject | oas31.ReferenceObject;
+
+  const length = Object.values(values).length;
+
   if (schema.type === 'ref') {
-    if (Object.values(metadata).every((val) => val === undefined)) {
+    if (length === 0) {
       return schema;
     }
 
+    if (length === 1 && metadata.description) {
+      return createDescriptionMetadata(schema, metadata.description, state);
+    }
+
     return {
       type: 'schema',
       schema: {
-        allOf: [schema.schema, metadata],
+        allOf: [schema.schema],
+        ...metadata,
       },
       effects: schema.effects,
     };
   }
 
+  // Calculate if we can extend the previous schema
+  if (previous && schema.schema.type !== 'object') {
+    const diff = Object.entries({ ...schema.schema, ...values }).reduce(
+      (acc, [key, value]) => {
+        if (
+          previous.schemaObject &&
+          isValueEqual(
+            (previous.schemaObject as Record<string, unknown>)[key],
+            value,
+          )
+        ) {
+          return acc;
+        }
+        acc[key] = value;
+
+        return acc;
+      },
+      {} as Record<string, unknown>,
+    );
+
+    const diffLength = Object.values(diff).length;
+
+    if (diffLength === 0) {
+      return {
+        type: 'ref',
+        schema: {
+          $ref: previous.schema.$ref,
+        },
+        effects: schema.effects,
+        schemaObject: previous.schemaObject,
+        zodType: previous.zodType,
+      };
+    }
+
+    if (diffLength === 1 && typeof diff.description === 'string') {
+      return createDescriptionMetadata(previous, diff.description, state);
+    }
+
+    return {
+      type: 'schema',
+      schema: { allOf: [previous.schema], ...diff },
+      effects: schema.effects,
+    };
+  }
+
   return {
     type: 'schema',
     schema: {
diff --git a/src/create/schema/parsers/catch.ts b/src/create/schema/parsers/catch.ts
index dba6dec..242f475 100644
--- a/src/create/schema/parsers/catch.ts
+++ b/src/create/schema/parsers/catch.ts
@@ -1,7 +1,8 @@
 import type { ZodCatch, ZodTypeAny } from 'zod';
 
-import type { oas31 } from '../../../../dist';
+import type { oas31 } from '../../../openapi3-ts/dist';
 import {
+  type RefObject,
   type Schema,
   type SchemaState,
   createSchemaObject,
@@ -11,6 +12,7 @@ import { enhanceWithMetadata } from '../metadata';
 export const createCatchSchema = <T extends ZodTypeAny>(
   zodCatch: ZodCatch<T>,
   state: SchemaState,
+  previous: RefObject | undefined,
 ): Schema => {
   const schemaObject = createSchemaObject(zodCatch._def.innerType, state, [
     'default',
@@ -18,14 +20,12 @@ export const createCatchSchema = <T extends ZodTypeAny>(
 
   const catchResult = zodCatch.safeParse(undefined);
 
-  const maybeDefaultValue: Pick<oas31.SchemaObject, 'default'> | undefined =
+  const maybeDefaultValue: Pick<oas31.SchemaObject, 'default'> =
     catchResult.success
       ? {
           default: catchResult.data,
         }
-      : undefined;
+      : {};
 
-  return enhanceWithMetadata(schemaObject, {
-    ...maybeDefaultValue,
-  });
+  return enhanceWithMetadata(schemaObject, maybeDefaultValue, state, previous);
 };
diff --git a/src/create/schema/parsers/default.ts b/src/create/schema/parsers/default.ts
index 7d9f90c..f1510bb 100644
--- a/src/create/schema/parsers/default.ts
+++ b/src/create/schema/parsers/default.ts
@@ -1,6 +1,7 @@
 import type { ZodDefault, ZodTypeAny } from 'zod';
 
 import {
+  type RefObject,
   type Schema,
   type SchemaState,
   createSchemaObject,
@@ -10,12 +11,18 @@ import { enhanceWithMetadata } from '../metadata';
 export const createDefaultSchema = <T extends ZodTypeAny>(
   zodDefault: ZodDefault<T>,
   state: SchemaState,
+  previous: RefObject | undefined,
 ): Schema => {
   const schemaObject = createSchemaObject(zodDefault._def.innerType, state, [
     'default',
   ]);
 
-  return enhanceWithMetadata(schemaObject, {
-    default: zodDefault._def.defaultValue(),
-  });
+  return enhanceWithMetadata(
+    schemaObject,
+    {
+      default: zodDefault._def.defaultValue(),
+    },
+    state,
+    previous,
+  );
 };
diff --git a/src/create/schema/parsers/index.ts b/src/create/schema/parsers/index.ts
index e5f6c45..4715eb6 100644
--- a/src/create/schema/parsers/index.ts
+++ b/src/create/schema/parsers/index.ts
@@ -1,7 +1,7 @@
 import type { ZodType, ZodTypeDef } from 'zod';
 
 import { isZodType } from '../../../zodType';
-import type { Schema, SchemaState } from '../../schema';
+import type { RefObject, Schema, SchemaState } from '../../schema';
 
 import { createArraySchema } from './array';
 import { createBooleanSchema } from './boolean';
@@ -39,9 +39,10 @@ export const createSchemaSwitch = <
   Input = Output,
 >(
   zodSchema: ZodType<Output, Def, Input>,
+  previous: RefObject | undefined,
   state: SchemaState,
 ): Schema => {
-  if (zodSchema._def.openapi?.type) {
+  if (zodSchema._def.zodOpenApi?.openapi?.type) {
     return createManualTypeSchema(zodSchema, state);
   }
 
@@ -74,7 +75,7 @@ export const createSchemaSwitch = <
   }
 
   if (isZodType(zodSchema, 'ZodObject')) {
-    return createObjectSchema(zodSchema, state);
+    return createObjectSchema(zodSchema, previous, state);
   }
 
   if (isZodType(zodSchema, 'ZodUnion')) {
@@ -102,7 +103,7 @@ export const createSchemaSwitch = <
   }
 
   if (isZodType(zodSchema, 'ZodDefault')) {
-    return createDefaultSchema(zodSchema, state);
+    return createDefaultSchema(zodSchema, state, previous);
   }
 
   if (isZodType(zodSchema, 'ZodRecord')) {
@@ -151,7 +152,7 @@ export const createSchemaSwitch = <
   }
 
   if (isZodType(zodSchema, 'ZodCatch')) {
-    return createCatchSchema(zodSchema, state);
+    return createCatchSchema(zodSchema, state, previous);
   }
 
   if (isZodType(zodSchema, 'ZodUnknown') || isZodType(zodSchema, 'ZodAny')) {
diff --git a/src/create/schema/parsers/manual.ts b/src/create/schema/parsers/manual.ts
index 406e3ea..7c89da1 100644
--- a/src/create/schema/parsers/manual.ts
+++ b/src/create/schema/parsers/manual.ts
@@ -10,7 +10,7 @@ export const createManualTypeSchema = <
   zodSchema: ZodType<Output, Def, Input>,
   state: SchemaState,
 ): Schema => {
-  if (!zodSchema._def.openapi?.type) {
+  if (!zodSchema._def.zodOpenApi?.openapi?.type) {
     const schemaName = zodSchema.constructor.name;
     throw new Error(
       `Unknown schema ${schemaName} at ${state.path.join(
@@ -22,7 +22,7 @@ export const createManualTypeSchema = <
   return {
     type: 'schema',
     schema: {
-      type: zodSchema._def.openapi.type,
+      type: zodSchema._def.zodOpenApi?.openapi.type,
     },
   };
 };
diff --git a/src/create/schema/parsers/object.ts b/src/create/schema/parsers/object.ts
index 13cdfed..10772d0 100644
--- a/src/create/schema/parsers/object.ts
+++ b/src/create/schema/parsers/object.ts
@@ -12,10 +12,12 @@ import type { oas31 } from '../../../openapi3-ts/dist';
 import { isZodType } from '../../../zodType';
 import { type Effect, createComponentSchemaRef } from '../../components';
 import {
+  type RefObject,
   type Schema,
   type SchemaState,
   createSchemaObject,
 } from '../../schema';
+import { createDescriptionMetadata } from '../metadata';
 
 import { isOptionalObjectKey } from './optional';
 import { flattenEffects } from './transform';
@@ -28,11 +30,12 @@ export const createObjectSchema = <
   Input = objectInputType<T, Catchall, UnknownKeys>,
 >(
   zodObject: ZodObject<T, UnknownKeys, Catchall, Output, Input>,
+  previous: RefObject | undefined,
   state: SchemaState,
 ): Schema => {
   const extendedSchema = createExtendedSchema(
     zodObject,
-    zodObject._def.extendMetadata?.extends,
+    previous?.zodType as ZodObject<T, UnknownKeys, Catchall, Output, Input>,
     state,
   );
 
@@ -66,7 +69,7 @@ export const createExtendedSchema = <
   }
 
   const component = state.components.schemas.get(baseZodObject);
-  if (component ?? baseZodObject._def.openapi?.ref) {
+  if (component ?? baseZodObject._def.zodOpenApi?.openapi?.ref) {
     createSchemaObject(baseZodObject, state, ['extended schema']);
   }
 
@@ -102,8 +105,64 @@ export const createExtendedSchema = <
     diffShape,
     diffOpts,
     state,
+    true,
   );
 
+  const schemaLength = Object.keys(extendedSchema.schema).length;
+  const effects = flattenEffects([
+    completeComponent.type === 'complete' ? completeComponent.effects : [],
+    completeComponent.type === 'in-progress'
+      ? [
+          {
+            type: 'component',
+            zodType: zodObject,
+            path: [...state.path],
+          },
+        ]
+      : [],
+    extendedSchema.effects,
+  ]);
+
+  if (schemaLength === 0) {
+    return {
+      type: 'ref',
+      schema: {
+        $ref: createComponentSchemaRef(
+          completeComponent.ref,
+          state.documentOptions?.componentRefPath,
+        ),
+      },
+      schemaObject:
+        completeComponent.type === 'complete'
+          ? completeComponent.schemaObject
+          : undefined,
+      zodType: zodObject,
+      effects,
+    };
+  }
+
+  if (schemaLength === 1 && extendedSchema.schema.description) {
+    return createDescriptionMetadata(
+      {
+        type: 'ref',
+        schema: {
+          $ref: createComponentSchemaRef(
+            completeComponent.ref,
+            state.documentOptions?.componentRefPath,
+          ),
+        },
+        schemaObject:
+          completeComponent.type === 'complete'
+            ? completeComponent.schemaObject
+            : undefined,
+        zodType: zodObject,
+        effects,
+      },
+      extendedSchema.schema.description,
+      state,
+    );
+  }
+
   return {
     type: 'schema',
     schema: {
@@ -182,6 +241,7 @@ export const createObjectSchemaFromShape = (
   shape: ZodRawShape,
   { unknownKeys, catchAll }: AdditionalPropertyOptions,
   state: SchemaState,
+  omitType?: boolean,
 ): Schema => {
   const properties = mapProperties(shape, state);
   const required = mapRequired(properties, shape, state);
@@ -192,7 +252,7 @@ export const createObjectSchemaFromShape = (
   return {
     type: 'schema',
     schema: {
-      type: 'object',
+      ...(!omitType && { type: 'object' }),
       ...(properties && { properties: properties.properties }),
       ...(required?.required.length && { required: required.required }),
       ...(unknownKeys === 'strict' && { additionalProperties: false }),
diff --git a/src/create/schema/parsers/pipeline.ts b/src/create/schema/parsers/pipeline.ts
index f974727..aa0cb54 100644
--- a/src/create/schema/parsers/pipeline.ts
+++ b/src/create/schema/parsers/pipeline.ts
@@ -16,13 +16,13 @@ export const createPipelineSchema = <
   state: SchemaState,
 ): Schema => {
   if (
-    zodPipeline._def.openapi?.effectType === 'input' ||
-    zodPipeline._def.openapi?.effectType === 'same'
+    zodPipeline._def.zodOpenApi?.openapi?.effectType === 'input' ||
+    zodPipeline._def.zodOpenApi?.openapi?.effectType === 'same'
   ) {
     return createSchemaObject(zodPipeline._def.in, state, ['pipeline input']);
   }
 
-  if (zodPipeline._def.openapi?.effectType === 'output') {
+  if (zodPipeline._def.zodOpenApi?.openapi?.effectType === 'output') {
     return createSchemaObject(zodPipeline._def.out, state, ['pipeline output']);
   }
 
diff --git a/src/create/schema/parsers/transform.ts b/src/create/schema/parsers/transform.ts
index 5928e8d..7e9d92c 100644
--- a/src/create/schema/parsers/transform.ts
+++ b/src/create/schema/parsers/transform.ts
@@ -24,7 +24,7 @@ export const createTransformSchema = <
   zodTransform: ZodEffects<T, Output, Input>,
   state: SchemaState,
 ): Schema => {
-  if (zodTransform._def.openapi?.effectType === 'output') {
+  if (zodTransform._def.zodOpenApi?.openapi?.effectType === 'output') {
     return {
       type: 'schema',
       schema: createManualOutputTransformSchema(zodTransform, state),
@@ -32,8 +32,8 @@ export const createTransformSchema = <
   }
 
   if (
-    zodTransform._def.openapi?.effectType === 'input' ||
-    zodTransform._def.openapi?.effectType === 'same'
+    zodTransform._def.zodOpenApi?.openapi?.effectType === 'input' ||
+    zodTransform._def.zodOpenApi?.openapi?.effectType === 'same'
   ) {
     return createSchemaObject(zodTransform._def.schema, state, [
       'transform input',
@@ -75,7 +75,7 @@ export const createManualOutputTransformSchema = <
   zodTransform: ZodEffects<T, Output, Input>,
   state: SchemaState,
 ): oas31.SchemaObject => {
-  if (!zodTransform._def.openapi?.type) {
+  if (!zodTransform._def.zodOpenApi?.openapi?.type) {
     const zodType = zodTransform.constructor.name;
     const schemaName = `${zodType} - ${zodTransform._def.effect.type}`;
     throw new Error(
@@ -86,7 +86,7 @@ export const createManualOutputTransformSchema = <
   }
 
   return {
-    type: zodTransform._def.openapi.type,
+    type: zodTransform._def.zodOpenApi?.openapi.type,
   };
 };
 
diff --git a/src/create/schema/parsers/union.ts b/src/create/schema/parsers/union.ts
index 2b57d7c..00e0c8d 100644
--- a/src/create/schema/parsers/union.ts
+++ b/src/create/schema/parsers/union.ts
@@ -22,7 +22,10 @@ export const createUnionSchema = <
     return acc;
   }, []);
 
-  if (zodUnion._def.openapi?.unionOneOf ?? state.documentOptions?.unionOneOf) {
+  if (
+    zodUnion._def.zodOpenApi?.openapi?.unionOneOf ??
+    state.documentOptions?.unionOneOf
+  ) {
     return {
       type: 'schema',
       schema: {
diff --git a/src/extendZod.ts b/src/extendZod.ts
index e457945..b97aa16 100644
--- a/src/extendZod.ts
+++ b/src/extendZod.ts
@@ -1,21 +1,81 @@
-import type { ZodRawShape, z } from 'zod';
+import type { ZodRawShape, ZodTypeDef, z } from 'zod';
+
 import './extendZodTypes';
 
+type ZodOpenApiMetadataDef = NonNullable<ZodTypeDef['zodOpenApi']>;
+type ZodOpenApiMetadata = ZodOpenApiMetadataDef['openapi'];
+
+const mergeOpenApi = (
+  openapi: ZodOpenApiMetadata,
+  {
+    ref: _ref,
+    refType: _refType,
+    param: _param,
+    header: _header,
+    ...rest
+  }: ZodOpenApiMetadata = {},
+) => ({
+  ...rest,
+  ...openapi,
+});
+
 export function extendZodWithOpenApi(zod: typeof z) {
   if (typeof zod.ZodType.prototype.openapi !== 'undefined') {
     return;
   }
+
   zod.ZodType.prototype.openapi = function (openapi) {
+    const { zodOpenApi, ...rest } = this._def as {
+      zodOpenApi?: ZodOpenApiMetadataDef;
+      [key: string]: unknown;
+    };
+
     // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
     const result = new (this as any).constructor({
-      ...this._def,
-      openapi,
+      ...rest,
+      zodOpenApi: {
+        openapi: mergeOpenApi(
+          openapi as unknown as ZodOpenApiMetadata,
+          zodOpenApi?.openapi,
+        ),
+      },
     });
 
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
+    result._def.zodOpenApi.current = result;
+
+    if (zodOpenApi) {
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+      result._def.zodOpenApi.previous = this;
+    }
+
     // eslint-disable-next-line @typescript-eslint/no-unsafe-return
     return result;
   };
 
+  // eslint-disable-next-line @typescript-eslint/unbound-method
+  const zodDescribe = zod.ZodType.prototype.describe;
+
+  zod.ZodType.prototype.describe = function (...args: [description: string]) {
+    const result = zodDescribe.apply(this, args);
+    const def = result._def as ZodTypeDef;
+
+    if (def.zodOpenApi) {
+      const cloned = { ...def.zodOpenApi };
+      cloned.openapi = mergeOpenApi({ description: args[0] }, cloned.openapi);
+      cloned.previous = this;
+      cloned.current = result;
+      def.zodOpenApi = cloned;
+    } else {
+      def.zodOpenApi = {
+        openapi: { description: args[0] },
+        current: result,
+      };
+    }
+
+    return result;
+  };
+
   // eslint-disable-next-line @typescript-eslint/unbound-method
   const zodObjectExtend = zod.ZodObject.prototype.extend;
 
@@ -23,10 +83,19 @@ export function extendZodWithOpenApi(zod: typeof z) {
     ...args: [augmentation: ZodRawShape]
   ) {
     const extendResult = zodObjectExtend.apply(this, args);
-    extendResult._def.extendMetadata = {
-      extends: this,
-    };
-    delete extendResult._def.openapi;
+
+    const zodOpenApi = extendResult._def.zodOpenApi;
+    if (zodOpenApi) {
+      const cloned = { ...zodOpenApi };
+      cloned.openapi = mergeOpenApi({}, cloned.openapi);
+      cloned.previous = this;
+      extendResult._def.zodOpenApi = cloned;
+    } else {
+      extendResult._def.zodOpenApi = {
+        previous: this,
+      };
+    }
+
     // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
     return extendResult as any;
   };
@@ -38,8 +107,15 @@ export function extendZodWithOpenApi(zod: typeof z) {
     ...args: [mask: Record<string, true | undefined>]
   ) {
     const omitResult = zodObjectOmit.apply(this, args);
-    delete omitResult._def.extendMetadata;
-    delete omitResult._def.openapi;
+
+    const zodOpenApi = omitResult._def.zodOpenApi;
+    if (zodOpenApi) {
+      const cloned = { ...zodOpenApi };
+      cloned.openapi = mergeOpenApi({}, cloned.openapi);
+      delete cloned.previous;
+      omitResult._def.zodOpenApi = cloned;
+    }
+
     // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
     return omitResult as any;
   };
@@ -51,8 +127,14 @@ export function extendZodWithOpenApi(zod: typeof z) {
     ...args: [mask: Record<string, true | undefined>]
   ) {
     const pickResult = zodObjectPick.apply(this, args);
-    delete pickResult._def.extendMetadata;
-    delete pickResult._def.openapi;
+
+    const zodOpenApi = pickResult._def.zodOpenApi;
+    if (zodOpenApi) {
+      const cloned = { ...zodOpenApi };
+      cloned.openapi = mergeOpenApi({}, cloned.openapi);
+      delete cloned.previous;
+      pickResult._def.zodOpenApi = cloned;
+    }
     // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
     return pickResult as any;
   };
diff --git a/src/extendZodTypes.ts b/src/extendZodTypes.ts
index 0b499df..af6e16b 100644
--- a/src/extendZodTypes.ts
+++ b/src/extendZodTypes.ts
@@ -67,6 +67,22 @@ interface ZodOpenApiMetadata<
   type?: SchemaObject['type'];
 }
 
+interface ZodOpenApiMetadataDef {
+  /**
+   * Up to date OpenAPI metadata
+   */
+  openapi?: ZodOpenApiMetadata<ZodTypeAny>;
+  /**
+   * Used to keep track of the Zod Schema had `.openapi` called on it
+   */
+  current?: ZodTypeAny;
+  /**
+   * Used to keep track of the previous Zod Schema that had `.openapi` called on it if another `.openapi` is called.
+   * This can also be present when .extend is called on an object.
+   */
+  previous?: ZodTypeAny;
+}
+
 interface ZodOpenApiExtendMetadata {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   extends: ZodObject<any, any, any, any, any>;
@@ -81,10 +97,7 @@ declare module 'zod' {
   }
 
   interface ZodTypeDef {
-    /**
-     * OpenAPI metadata
-     */
-    openapi?: ZodOpenApiMetadata<ZodTypeAny>;
+    zodOpenApi?: ZodOpenApiMetadataDef;
   }
 
   export interface ZodObjectDef {
diff --git a/src/openapi.ts b/src/openapi.ts
index 03e425f..decfc02 100644
--- a/src/openapi.ts
+++ b/src/openapi.ts
@@ -1,4 +1,4 @@
-import type { oas31 } from './openapi3-ts/dist';
+import type { oas30, oas31 } from './openapi3-ts/dist';
 
 export const openApiVersions = [
   '3.0.0',
@@ -16,6 +16,10 @@ export const satisfiesVersion = (
 ) => openApiVersions.indexOf(test) >= openApiVersions.indexOf(against);
 
 export const isReferenceObject = (
-  schemaOrRef: oas31.SchemaObject | oas31.ReferenceObject,
-): schemaOrRef is oas31.ReferenceObject =>
+  schemaOrRef:
+    | oas31.SchemaObject
+    | oas31.ReferenceObject
+    | oas30.SchemaObject
+    | oas30.ReferenceObject,
+): schemaOrRef is oas31.ReferenceObject | oas30.ReferenceObject =>
   Boolean('$ref' in schemaOrRef && schemaOrRef.$ref);


Test patch:
diff --git a/src/create/document.test.ts b/src/create/document.test.ts
index ae7d3b0..537c92d 100644
--- a/src/create/document.test.ts
+++ b/src/create/document.test.ts
@@ -518,7 +518,6 @@ describe('createDocument', () => {
         "required": [
           "d",
         ],
-        "type": "object",
       },
       "lazy": {
         "items": {
@@ -852,7 +851,6 @@ describe('createDocument', () => {
         "required": [
           "d",
         ],
-        "type": "object",
       },
       "lazy": {
         "items": {
diff --git a/src/create/schema/metadata.test.ts b/src/create/schema/metadata.test.ts
index 51f35ce..faf74ad 100644
--- a/src/create/schema/metadata.test.ts
+++ b/src/create/schema/metadata.test.ts
@@ -1,7 +1,11 @@
 import '../../entries/extend';
 import { z } from 'zod';
 
-import { createOutputState } from '../../testing/state';
+import {
+  createInputState,
+  createOutputOpenapi3State,
+  createOutputState,
+} from '../../testing/state';
 
 import { type Schema, createSchemaObject } from './index';
 
@@ -62,6 +66,10 @@ describe('enhanceWithMetadata', () => {
         $ref: '#/components/schemas/bla',
       },
       zodType: blaSchema,
+      schemaObject: {
+        type: 'string',
+        description: 'bla',
+      },
     };
 
     const result = createSchemaObject(fooSchema, createOutputState(), []);
@@ -83,7 +91,31 @@ describe('enhanceWithMetadata', () => {
     expect(result).toEqual(expected);
   });
 
-  it('adds allOf to $refs', () => {
+  it('adds adds a description alongside $ref when only description is added in 3.1.0', () => {
+    const ref = z.string().openapi({ ref: 'ref' });
+
+    const expected: Schema = {
+      type: 'ref',
+      schema: {
+        $ref: '#/components/schemas/ref',
+        description: 'hello',
+      },
+      zodType: ref,
+      schemaObject: {
+        type: 'string',
+      },
+    };
+
+    const schema = ref.optional().openapi({ description: 'hello' });
+
+    const result = createSchemaObject(schema, createOutputState(), []);
+
+    expect(result).toEqual(expected);
+  });
+
+  it('adds adds a description alongside allOf when only description is added in 3.0.0', () => {
+    const ref = z.string().openapi({ ref: 'ref' });
+
     const expected: Schema = {
       type: 'schema',
       schema: {
@@ -91,18 +123,14 @@ describe('enhanceWithMetadata', () => {
           {
             $ref: '#/components/schemas/ref',
           },
-          {
-            description: 'hello',
-          },
         ],
+        description: 'hello',
       },
     };
 
-    const ref = z.string().openapi({ ref: 'ref' });
-
     const schema = ref.optional().openapi({ description: 'hello' });
 
-    const result = createSchemaObject(schema, createOutputState(), []);
+    const result = createSchemaObject(schema, createOutputOpenapi3State(), []);
 
     expect(result).toEqual(expected);
   });
@@ -115,6 +143,9 @@ describe('enhanceWithMetadata', () => {
         $ref: '#/components/schemas/og',
       },
       zodType: ref,
+      schemaObject: {
+        type: 'string',
+      },
     };
 
     const schema = ref.optional();
@@ -132,10 +163,8 @@ describe('enhanceWithMetadata', () => {
           {
             $ref: '#/components/schemas/ref2',
           },
-          {
-            default: 'a',
-          },
         ],
+        default: 'a',
       },
     };
 
@@ -155,7 +184,6 @@ describe('enhanceWithMetadata', () => {
         properties: {
           b: {
             allOf: [{ $ref: '#/components/schemas/a' }],
-            type: 'object',
             properties: { b: { type: 'string' } },
             required: ['b'],
             description: 'jello',
@@ -174,4 +202,696 @@ describe('enhanceWithMetadata', () => {
 
     expect(result).toEqual(expected);
   });
+
+  it('handles current and previous schemas', () => {
+    const FooSchema = z.string().openapi({ ref: 'foo' });
+    const CazSchema = z.object({ a: z.string() }).openapi({ ref: 'caz' });
+    const QuxSchema = CazSchema.extend({ b: FooSchema }).openapi({
+      ref: 'qux',
+    });
+
+    const BarSchema = z.object({
+      a: FooSchema.optional(),
+      b: FooSchema.openapi({ description: 'bar' }).optional(),
+      c: FooSchema.min(1).max(10),
+      d: FooSchema.openapi({ description: 'bar' }).min(1).max(10),
+      e: FooSchema.catch('a'),
+      f: FooSchema.default('a'),
+      g: FooSchema.email(),
+      h: FooSchema.datetime(),
+      i: FooSchema.openapi({ example: 'foo' }).min(1).max(10),
+      j: FooSchema.optional().openapi({ description: 'bar' }),
+      k: FooSchema.nullish().openapi({ description: 'bar' }),
+      l: FooSchema.describe('bar'),
+      m: CazSchema.openapi({ description: 'bar' }),
+      n: QuxSchema,
+      o: QuxSchema.extend({ c: FooSchema }).openapi({ description: 'qux' }),
+    });
+
+    const outputState = createOutputState();
+    const inputState = createInputState();
+    const result = createSchemaObject(BarSchema, outputState, []);
+    const inputResult = createSchemaObject(BarSchema, inputState, []);
+
+    expect(result).toMatchInlineSnapshot(`
+{
+  "effects": [
+    {
+      "creationType": "output",
+      "path": [
+        "property: e",
+      ],
+      "type": "schema",
+      "zodType": ZodCatch {
+        "_def": {
+          "catchValue": [Function],
+          "description": undefined,
+          "errorMap": [Function],
+          "innerType": ZodString {
+            "_def": {
+              "checks": [],
+              "coerce": false,
+              "typeName": "ZodString",
+              "zodOpenApi": {
+                "current": [Circular],
+                "openapi": {
+                  "ref": "foo",
+                },
+              },
+            },
+            "and": [Function],
+            "array": [Function],
+            "brand": [Function],
+            "catch": [Function],
+            "default": [Function],
+            "describe": [Function],
+            "isNullable": [Function],
+            "isOptional": [Function],
+            "nullable": [Function],
+            "nullish": [Function],
+            "optional": [Function],
+            "or": [Function],
+            "parse": [Function],
+            "parseAsync": [Function],
+            "pipe": [Function],
+            "promise": [Function],
+            "readonly": [Function],
+            "refine": [Function],
+            "refinement": [Function],
+            "safeParse": [Function],
+            "safeParseAsync": [Function],
+            "spa": [Function],
+            "superRefine": [Function],
+            "transform": [Function],
+          },
+          "typeName": "ZodCatch",
+        },
+        "and": [Function],
+        "array": [Function],
+        "brand": [Function],
+        "catch": [Function],
+        "default": [Function],
+        "describe": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "or": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "pipe": [Function],
+        "promise": [Function],
+        "readonly": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    {
+      "creationType": "output",
+      "path": [
+        "property: f",
+      ],
+      "type": "schema",
+      "zodType": ZodDefault {
+        "_def": {
+          "defaultValue": [Function],
+          "description": undefined,
+          "errorMap": [Function],
+          "innerType": ZodString {
+            "_def": {
+              "checks": [],
+              "coerce": false,
+              "typeName": "ZodString",
+              "zodOpenApi": {
+                "current": [Circular],
+                "openapi": {
+                  "ref": "foo",
+                },
+              },
+            },
+            "and": [Function],
+            "array": [Function],
+            "brand": [Function],
+            "catch": [Function],
+            "default": [Function],
+            "describe": [Function],
+            "isNullable": [Function],
+            "isOptional": [Function],
+            "nullable": [Function],
+            "nullish": [Function],
+            "optional": [Function],
+            "or": [Function],
+            "parse": [Function],
+            "parseAsync": [Function],
+            "pipe": [Function],
+            "promise": [Function],
+            "readonly": [Function],
+            "refine": [Function],
+            "refinement": [Function],
+            "safeParse": [Function],
+            "safeParseAsync": [Function],
+            "spa": [Function],
+            "superRefine": [Function],
+            "transform": [Function],
+          },
+          "typeName": "ZodDefault",
+        },
+        "and": [Function],
+        "array": [Function],
+        "brand": [Function],
+        "catch": [Function],
+        "default": [Function],
+        "describe": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "or": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "pipe": [Function],
+        "promise": [Function],
+        "readonly": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+  ],
+  "schema": {
+    "properties": {
+      "a": {
+        "$ref": "#/components/schemas/foo",
+      },
+      "b": {
+        "$ref": "#/components/schemas/foo",
+        "description": "bar",
+      },
+      "c": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "maxLength": 10,
+        "minLength": 1,
+      },
+      "d": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "description": "bar",
+        "maxLength": 10,
+        "minLength": 1,
+      },
+      "e": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "default": "a",
+      },
+      "f": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "default": "a",
+      },
+      "g": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "format": "email",
+      },
+      "h": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "format": "date-time",
+      },
+      "i": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "example": "foo",
+        "maxLength": 10,
+        "minLength": 1,
+      },
+      "j": {
+        "$ref": "#/components/schemas/foo",
+        "description": "bar",
+      },
+      "k": {
+        "description": "bar",
+        "oneOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+          {
+            "type": "null",
+          },
+        ],
+      },
+      "l": {
+        "$ref": "#/components/schemas/foo",
+        "description": "bar",
+      },
+      "m": {
+        "$ref": "#/components/schemas/caz",
+        "description": "bar",
+      },
+      "n": {
+        "$ref": "#/components/schemas/qux",
+      },
+      "o": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/qux",
+          },
+        ],
+        "description": "qux",
+        "properties": {
+          "c": {
+            "$ref": "#/components/schemas/foo",
+          },
+        },
+        "required": [
+          "c",
+        ],
+      },
+    },
+    "required": [
+      "c",
+      "d",
+      "e",
+      "f",
+      "g",
+      "h",
+      "i",
+      "l",
+      "m",
+      "n",
+      "o",
+    ],
+    "type": "object",
+  },
+  "type": "schema",
+}
+`);
+    expect(
+      Array.from(outputState.components.schemas.entries(), ([, value]) => ({
+        [value.ref]: value.type === 'complete' ? value.schemaObject : null,
+      })),
+    ).toMatchInlineSnapshot(`
+[
+  {
+    "foo": {
+      "type": "string",
+    },
+  },
+  {
+    "caz": {
+      "properties": {
+        "a": {
+          "type": "string",
+        },
+      },
+      "required": [
+        "a",
+      ],
+      "type": "object",
+    },
+  },
+  {
+    "qux": {
+      "allOf": [
+        {
+          "$ref": "#/components/schemas/caz",
+        },
+      ],
+      "properties": {
+        "b": {
+          "$ref": "#/components/schemas/foo",
+        },
+      },
+      "required": [
+        "b",
+      ],
+    },
+  },
+]
+`);
+
+    expect(inputResult).toMatchInlineSnapshot(`
+{
+  "effects": [
+    {
+      "creationType": "input",
+      "path": [
+        "property: e",
+      ],
+      "type": "schema",
+      "zodType": ZodCatch {
+        "_def": {
+          "catchValue": [Function],
+          "description": undefined,
+          "errorMap": [Function],
+          "innerType": ZodString {
+            "_def": {
+              "checks": [],
+              "coerce": false,
+              "typeName": "ZodString",
+              "zodOpenApi": {
+                "current": [Circular],
+                "openapi": {
+                  "ref": "foo",
+                },
+              },
+            },
+            "and": [Function],
+            "array": [Function],
+            "brand": [Function],
+            "catch": [Function],
+            "default": [Function],
+            "describe": [Function],
+            "isNullable": [Function],
+            "isOptional": [Function],
+            "nullable": [Function],
+            "nullish": [Function],
+            "optional": [Function],
+            "or": [Function],
+            "parse": [Function],
+            "parseAsync": [Function],
+            "pipe": [Function],
+            "promise": [Function],
+            "readonly": [Function],
+            "refine": [Function],
+            "refinement": [Function],
+            "safeParse": [Function],
+            "safeParseAsync": [Function],
+            "spa": [Function],
+            "superRefine": [Function],
+            "transform": [Function],
+          },
+          "typeName": "ZodCatch",
+        },
+        "and": [Function],
+        "array": [Function],
+        "brand": [Function],
+        "catch": [Function],
+        "default": [Function],
+        "describe": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "or": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "pipe": [Function],
+        "promise": [Function],
+        "readonly": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    {
+      "creationType": "input",
+      "path": [
+        "property: f",
+      ],
+      "type": "schema",
+      "zodType": ZodDefault {
+        "_def": {
+          "defaultValue": [Function],
+          "description": undefined,
+          "errorMap": [Function],
+          "innerType": ZodString {
+            "_def": {
+              "checks": [],
+              "coerce": false,
+              "typeName": "ZodString",
+              "zodOpenApi": {
+                "current": [Circular],
+                "openapi": {
+                  "ref": "foo",
+                },
+              },
+            },
+            "and": [Function],
+            "array": [Function],
+            "brand": [Function],
+            "catch": [Function],
+            "default": [Function],
+            "describe": [Function],
+            "isNullable": [Function],
+            "isOptional": [Function],
+            "nullable": [Function],
+            "nullish": [Function],
+            "optional": [Function],
+            "or": [Function],
+            "parse": [Function],
+            "parseAsync": [Function],
+            "pipe": [Function],
+            "promise": [Function],
+            "readonly": [Function],
+            "refine": [Function],
+            "refinement": [Function],
+            "safeParse": [Function],
+            "safeParseAsync": [Function],
+            "spa": [Function],
+            "superRefine": [Function],
+            "transform": [Function],
+          },
+          "typeName": "ZodDefault",
+        },
+        "and": [Function],
+        "array": [Function],
+        "brand": [Function],
+        "catch": [Function],
+        "default": [Function],
+        "describe": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "or": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "pipe": [Function],
+        "promise": [Function],
+        "readonly": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+  ],
+  "schema": {
+    "properties": {
+      "a": {
+        "$ref": "#/components/schemas/foo",
+      },
+      "b": {
+        "$ref": "#/components/schemas/foo",
+        "description": "bar",
+      },
+      "c": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "maxLength": 10,
+        "minLength": 1,
+      },
+      "d": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "description": "bar",
+        "maxLength": 10,
+        "minLength": 1,
+      },
+      "e": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "default": "a",
+      },
+      "f": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "default": "a",
+      },
+      "g": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "format": "email",
+      },
+      "h": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "format": "date-time",
+      },
+      "i": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+        ],
+        "example": "foo",
+        "maxLength": 10,
+        "minLength": 1,
+      },
+      "j": {
+        "$ref": "#/components/schemas/foo",
+        "description": "bar",
+      },
+      "k": {
+        "description": "bar",
+        "oneOf": [
+          {
+            "$ref": "#/components/schemas/foo",
+          },
+          {
+            "type": "null",
+          },
+        ],
+      },
+      "l": {
+        "$ref": "#/components/schemas/foo",
+        "description": "bar",
+      },
+      "m": {
+        "$ref": "#/components/schemas/caz",
+        "description": "bar",
+      },
+      "n": {
+        "$ref": "#/components/schemas/qux",
+      },
+      "o": {
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/qux",
+          },
+        ],
+        "description": "qux",
+        "properties": {
+          "c": {
+            "$ref": "#/components/schemas/foo",
+          },
+        },
+        "required": [
+          "c",
+        ],
+      },
+    },
+    "required": [
+      "c",
+      "d",
+      "g",
+      "h",
+      "i",
+      "l",
+      "m",
+      "n",
+      "o",
+    ],
+    "type": "object",
+  },
+  "type": "schema",
+}
+`);
+
+    expect(
+      Array.from(inputState.components.schemas.entries(), ([, value]) => ({
+        [value.ref]: value.type === 'complete' ? value.schemaObject : null,
+      })),
+    ).toMatchInlineSnapshot(`
+[
+  {
+    "foo": {
+      "type": "string",
+    },
+  },
+  {
+    "caz": {
+      "properties": {
+        "a": {
+          "type": "string",
+        },
+      },
+      "required": [
+        "a",
+      ],
+      "type": "object",
+    },
+  },
+  {
+    "qux": {
+      "allOf": [
+        {
+          "$ref": "#/components/schemas/caz",
+        },
+      ],
+      "properties": {
+        "b": {
+          "$ref": "#/components/schemas/foo",
+        },
+      },
+      "required": [
+        "b",
+      ],
+    },
+  },
+]
+`);
+  });
 });
diff --git a/src/create/schema/parsers/catch.test.ts b/src/create/schema/parsers/catch.test.ts
index 3aff702..894e3bc 100644
--- a/src/create/schema/parsers/catch.test.ts
+++ b/src/create/schema/parsers/catch.test.ts
@@ -17,7 +17,7 @@ describe('createCatchSchema', () => {
     };
     const schema = z.string().catch('bob');
 
-    const result = createCatchSchema(schema, createOutputState());
+    const result = createCatchSchema(schema, createOutputState(), undefined);
 
     expect(result).toEqual(expected);
   });
diff --git a/src/create/schema/parsers/default.test.ts b/src/create/schema/parsers/default.test.ts
index aa346de..1f8da1f 100644
--- a/src/create/schema/parsers/default.test.ts
+++ b/src/create/schema/parsers/default.test.ts
@@ -17,7 +17,7 @@ describe('createDefaultSchema', () => {
     };
     const schema = z.string().default('a');
 
-    const result = createDefaultSchema(schema, createOutputState());
+    const result = createDefaultSchema(schema, createOutputState(), undefined);
 
     expect(result).toEqual(expected);
   });
@@ -30,15 +30,13 @@ describe('createDefaultSchema', () => {
           {
             $ref: '#/components/schemas/ref',
           },
-          {
-            default: 'a',
-          },
         ],
+        default: 'a',
       },
     };
     const schema = z.string().openapi({ ref: 'ref' }).optional().default('a');
 
-    const result = createDefaultSchema(schema, createOutputState());
+    const result = createDefaultSchema(schema, createOutputState(), undefined);
 
     expect(result).toEqual(expected);
   });
diff --git a/src/create/schema/parsers/lazy.test.ts b/src/create/schema/parsers/lazy.test.ts
index 445b376..e135612 100644
--- a/src/create/schema/parsers/lazy.test.ts
+++ b/src/create/schema/parsers/lazy.test.ts
@@ -132,6 +132,7 @@ describe('createLazySchema', () => {
 
     const result = createObjectSchema(
       UserSchema as ZodObject<any, any, any, any, any>,
+      undefined,
       state,
     );
 
@@ -210,7 +211,11 @@ describe('createLazySchema', () => {
       ref: 'post',
     });
 
-    const result = createNewSchema(UserSchema, state);
+    const result = createNewSchema({
+      zodSchema: UserSchema,
+      previous: undefined,
+      state,
+    });
 
     expect(result.schema).toEqual(expected);
   });
@@ -258,7 +263,7 @@ describe('createLazySchema', () => {
 
     const state = createOutputState();
 
-    const result = createObjectSchema(ContainerSchema, state);
+    const result = createObjectSchema(ContainerSchema, undefined, state);
 
     expect(result).toEqual(expected);
 
diff --git a/src/create/schema/parsers/nullable.test.ts b/src/create/schema/parsers/nullable.test.ts
index 513a449..528855d 100644
--- a/src/create/schema/parsers/nullable.test.ts
+++ b/src/create/schema/parsers/nullable.test.ts
@@ -88,7 +88,6 @@ describe('createNullableSchema', () => {
         properties: {
           b: {
             allOf: [{ $ref: '#/components/schemas/a' }],
-            type: 'object',
             properties: { b: { type: 'string' } },
             required: ['b'],
             nullable: true,
@@ -215,7 +214,6 @@ describe('createNullableSchema', () => {
               oneOf: [
                 {
                   allOf: [{ $ref: '#/components/schemas/a' }],
-                  type: 'object',
                   properties: { b: { type: 'string' } },
                   required: ['b'],
                 },
diff --git a/src/create/schema/parsers/object.test.ts b/src/create/schema/parsers/object.test.ts
index a526f40..8f35026 100644
--- a/src/create/schema/parsers/object.test.ts
+++ b/src/create/schema/parsers/object.test.ts
@@ -24,7 +24,7 @@ describe('createObjectSchema', () => {
       b: z.string().optional(),
     });
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
@@ -47,7 +47,7 @@ describe('createObjectSchema', () => {
       a: z.string(),
     });
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
@@ -70,7 +70,9 @@ describe('createObjectSchema', () => {
       })
       .catchall(z.boolean());
 
-    expect(createObjectSchema(schema, createOutputState())).toEqual(expected);
+    expect(createObjectSchema(schema, undefined, createOutputState())).toEqual(
+      expected,
+    );
   });
 
   it('considers ZodDefault in an input state as an effect', () => {
@@ -96,7 +98,7 @@ describe('createObjectSchema', () => {
       ],
     };
 
-    const result = createObjectSchema(schema, createInputState());
+    const result = createObjectSchema(schema, undefined, createInputState());
 
     expect(result).toEqual(expected);
   });
@@ -124,7 +126,7 @@ describe('createObjectSchema', () => {
       ],
     };
 
-    const result = createObjectSchema(schema, createInputState());
+    const result = createObjectSchema(schema, undefined, createInputState());
 
     expect(result).toEqual(expected);
   });
@@ -153,7 +155,7 @@ describe('createObjectSchema', () => {
       ],
     };
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
@@ -182,7 +184,7 @@ describe('createObjectSchema', () => {
       ],
     };
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
@@ -202,7 +204,7 @@ describe('required', () => {
         h: z.string().catch('a'),
       });
 
-      const result = createObjectSchema(schema, createOutputState());
+      const result = createObjectSchema(schema, undefined, createOutputState());
 
       expect(result).toEqual<Schema>({
         effects: expect.any(Array),
@@ -246,7 +248,7 @@ describe('required', () => {
           .pipe(z.number().optional()),
       });
 
-      const result = createObjectSchema(schema, createOutputState());
+      const result = createObjectSchema(schema, undefined, createOutputState());
 
       expect(result).toEqual<Schema>({
         effects: expect.any(Array),
@@ -280,7 +282,7 @@ describe('required', () => {
         f: z.custom((r) => r !== undefined),
       });
 
-      const result = createObjectSchema(schema, createInputState());
+      const result = createObjectSchema(schema, undefined, createInputState());
 
       expect(result).toEqual<Schema>({
         type: 'schema',
@@ -322,7 +324,7 @@ describe('required', () => {
         m: z.string().default('a'),
       });
 
-      const result = createObjectSchema(schema, createInputState());
+      const result = createObjectSchema(schema, undefined, createInputState());
 
       expect(result).toEqual<Schema>({
         effects: expect.any(Array),
@@ -357,7 +359,6 @@ describe('extend', () => {
           obj1: { $ref: '#/components/schemas/obj1' },
           obj2: {
             allOf: [{ $ref: '#/components/schemas/obj1' }],
-            type: 'object',
             properties: { b: { type: 'string' } },
             required: ['b'],
           },
@@ -372,7 +373,7 @@ describe('extend', () => {
       obj2: object2,
     });
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
@@ -406,7 +407,7 @@ describe('extend', () => {
       obj2: object2,
     });
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
@@ -420,7 +421,6 @@ describe('extend', () => {
           obj1: { $ref: '#/components/schemas/obj1' },
           obj2: {
             allOf: [{ $ref: '#/components/schemas/obj1' }],
-            type: 'object',
             properties: { b: { type: 'number' } },
             required: ['b'],
             additionalProperties: {
@@ -438,7 +438,7 @@ describe('extend', () => {
       obj2: object2,
     });
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
@@ -452,7 +452,7 @@ describe('extend', () => {
     });
 
     const state = createOutputState();
-    createObjectSchema(schema, state);
+    createObjectSchema(schema, undefined, state);
 
     expect(state.components.schemas.get(object1)?.ref).toBe('obj1');
     expect(state.components.schemas.get(object1)?.type).toBe('complete');
@@ -481,7 +481,7 @@ describe('extend', () => {
       obj2: object2,
     });
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
@@ -518,7 +518,7 @@ describe('extend', () => {
       b: zodDate,
     });
 
-    const result = createObjectSchema(schema, createOutputState());
+    const result = createObjectSchema(schema, undefined, createOutputState());
 
     expect(result).toEqual(expected);
   });
diff --git a/src/extendZod.test.ts b/src/extendZod.test.ts
index 09b2702..faea140 100644
--- a/src/extendZod.test.ts
+++ b/src/extendZod.test.ts
@@ -14,40 +14,57 @@ describe('extendZodWithOpenApi', () => {
     expect(() => run()).not.toThrow();
   });
 
-  it('adds ._def.openapi fields to a zod type', () => {
+  it('allows .openapi() to be chained in Zod Types', () => {
+    const a = z.string().openapi({ description: 'test' });
+    const b = a.openapi({ description: 'test2' });
+
+    expect(a._def.zodOpenApi?.openapi?.description).toBe('test');
+    expect(b._def.zodOpenApi?.openapi?.description).toBe('test2');
+    expect(
+      b._def.zodOpenApi?.previous?._def.zodOpenApi?.openapi?.description,
+    ).toBe('test');
+  });
+
+  it('sets current metadata when a schema is used again', () => {
+    const a = z.string().openapi({ ref: 'a' });
+    const b = a.uuid();
+
+    expect(a._def.zodOpenApi?.current).toBe(a);
+    expect(b._def.zodOpenApi?.current).toBe(a);
+  });
+
+  it('adds ._def.zodOpenApi.openapi fields to a zod type', () => {
     const a = z.string().openapi({ description: 'a' });
     const b = z.number().openapi({ examples: [1] });
 
-    expect(a._def.openapi?.description).toBe('a');
-    expect(b._def.openapi?.examples).toStrictEqual([1]);
+    expect(a._def.zodOpenApi?.openapi?.description).toBe('a');
+    expect(b._def.zodOpenApi?.openapi?.examples).toStrictEqual([1]);
   });
 
   it('adds persists .openapi() across some methods', () => {
     const a = z.string().openapi({ description: 'a' }).uuid();
 
-    expect(a._def.openapi?.description).toBe('a');
+    expect(a._def.zodOpenApi?.openapi?.description).toBe('a');
   });
 
   it('adds extendsMetadata to an object when .extend is used', () => {
     const a = z.object({ a: z.string() }).openapi({ ref: 'a' });
     const b = a.extend({ b: z.string() });
 
-    expect(a._def.openapi?.ref).toBe('a');
-    expect(b._def.extendMetadata?.extends).toStrictEqual(a);
+    expect(a._def.zodOpenApi?.openapi?.ref).toBe('a');
+    expect(b._def.zodOpenApi?.previous).toStrictEqual(a);
   });
 
-  it('adds removes extendsMetadata to an object when .omit or .pick is used', () => {
+  it('removes previous openapi ref for an object when .omit or .pick is used', () => {
     const a = z.object({ a: z.string() }).openapi({ ref: 'a' });
     const b = a.extend({ b: z.string() });
     const c = b.pick({ a: true });
     const d = b.omit({ a: true });
 
-    expect(a._def.openapi?.ref).toBe('a');
-    expect(b._def.extendMetadata?.extends).toStrictEqual(a);
-    expect(c._def.extendMetadata).toBeUndefined();
-    expect(c._def.openapi).toBeUndefined();
-    expect(d._def.extendMetadata).toBeUndefined();
-    expect(d._def.openapi).toBeUndefined();
+    expect(a._def.zodOpenApi?.openapi?.ref).toBe('a');
+    expect(b._def.zodOpenApi?.previous).toStrictEqual(a);
+    expect(c._def.zodOpenApi?.openapi).toEqual({});
+    expect(d._def.zodOpenApi?.openapi).toEqual({});
   });
 
   it("allows 'same' effectType when the input and output are equal", () => {
@@ -60,4 +77,16 @@ describe('extendZodWithOpenApi', () => {
       // @ts-expect-error - This is to test the effectType
       .openapi({ effectType: 'same' });
   });
+
+  it('preserves properties when using .openapi consecutively', () => {
+    const fooString = z
+      .string()
+      .regex(/^foo/)
+      .transform((string) => string.toUpperCase())
+      .openapi({ effectType: 'input' });
+
+    const barString = fooString.openapi({ description: 'foo' });
+
+    expect(barString._def.zodOpenApi?.openapi?.effectType).toBe('input');
+  });
 });