Repository: samchungy/zod-openapi
Pull request #405 (2025-01-23 23:33:21)

Explicitly set additionalProperties for `.passthrough()`
Sets `additionalProperties: true` to explicitly `true` when `.passthrough()` is added to a ZodObject.

https://github.com/fastify/fast-json-stringify#additional-properties

https://github.com/samchungy/fastify-zod-openapi/issues/224


Patch:
diff --git a/src/create/schema/parsers/object.ts b/src/create/schema/parsers/object.ts
index 10772d0..dc53819 100644
--- a/src/create/schema/parsers/object.ts
+++ b/src/create/schema/parsers/object.ts
@@ -237,6 +237,25 @@ interface AdditionalPropertyOptions {
   catchAll: ZodType;
 }
 
+const mapAdditionalProperties = (
+  { unknownKeys, catchAll }: AdditionalPropertyOptions,
+  state: SchemaState,
+) => {
+  if (!isZodType(catchAll, 'ZodNever')) {
+    return createSchemaObject(catchAll, state, ['additional properties']);
+  }
+
+  if (unknownKeys === 'strict') {
+    return false;
+  }
+
+  if (unknownKeys === 'passthrough') {
+    return true;
+  }
+
+  return undefined;
+};
+
 export const createObjectSchemaFromShape = (
   shape: ZodRawShape,
   { unknownKeys, catchAll }: AdditionalPropertyOptions,
@@ -245,9 +264,10 @@ export const createObjectSchemaFromShape = (
 ): Schema => {
   const properties = mapProperties(shape, state);
   const required = mapRequired(properties, shape, state);
-  const additionalProperties = !isZodType(catchAll, 'ZodNever')
-    ? createSchemaObject(catchAll, state, ['additional properties'])
-    : undefined;
+  const additionalProperties = mapAdditionalProperties(
+    { catchAll, unknownKeys },
+    state,
+  );
 
   return {
     type: 'schema',
@@ -255,14 +275,16 @@ export const createObjectSchemaFromShape = (
       ...(!omitType && { type: 'object' }),
       ...(properties && { properties: properties.properties }),
       ...(required?.required.length && { required: required.required }),
-      ...(unknownKeys === 'strict' && { additionalProperties: false }),
-      ...(additionalProperties && {
-        additionalProperties: additionalProperties.schema,
+      ...(additionalProperties !== undefined && {
+        additionalProperties:
+          typeof additionalProperties === 'object'
+            ? additionalProperties.schema
+            : additionalProperties,
       }),
     },
     effects: flattenEffects([
       ...(properties?.effects ?? []),
-      additionalProperties?.effects,
+      typeof additionalProperties === 'object' && additionalProperties?.effects,
       required?.effects,
     ]),
   };
diff --git a/src/create/schema/parsers/transform.ts b/src/create/schema/parsers/transform.ts
index 7e9d92c..a1b85b5 100644
--- a/src/create/schema/parsers/transform.ts
+++ b/src/create/schema/parsers/transform.ts
@@ -225,7 +225,9 @@ export const verifyEffects = (effects: Effect[], state: SchemaState) => {
   }
 };
 
-export const flattenEffects = (effects: Array<Effect[] | undefined>) => {
+export const flattenEffects = (
+  effects: Array<Effect[] | undefined | false>,
+) => {
   const allEffects = effects.reduce<Effect[]>((acc, effect) => {
     if (effect) {
       return acc.concat(effect);


Test patch:
diff --git a/src/create/schema/parsers/object.test.ts b/src/create/schema/parsers/object.test.ts
index 8f35026..ce571f5 100644
--- a/src/create/schema/parsers/object.test.ts
+++ b/src/create/schema/parsers/object.test.ts
@@ -52,6 +52,35 @@ describe('createObjectSchema', () => {
     expect(result).toEqual(expected);
   });
 
+  it('supports passthrough', () => {
+    const expected: Schema = {
+      type: 'schema',
+      schema: {
+        type: 'object',
+        properties: {
+          a: {
+            type: 'string',
+          },
+        },
+        required: ['a'],
+        additionalProperties: true,
+      },
+    };
+    const schema = z
+      .object({
+        a: z.string(),
+      })
+      .passthrough();
+
+    const result = createObjectSchema(
+      schema,
+      undefined,
+      createOutputState(undefined),
+    );
+
+    expect(result).toEqual(expected);
+  });
+
   it('supports catchall', () => {
     const expected: Schema = {
       type: 'schema',