You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

递归转换Zod Schema时丢失OpenAPI元数据,如何保留?

如何在递归转换Zod Schema时保留OpenAPI元数据?

我使用@asteasolutions/zod-to-openapi从Zod Schema生成OpenAPI文档,编写了递归工具函数makeSchemaDeepNullable将所有字段转为可空类型,但生成的文档丢失了所有OpenAPI元数据(描述、示例等)。

问题详情

应用makeSchemaDeepNullable转换Schema后,原始Schema中通过.openapi()定义的字段描述、示例等元数据无法保留到最终API文档中,仅顶层的描述能正常显示。

代码示例

带有OpenAPI元数据的原始Department Schema(示例)

// 包含OpenAPI元数据的部门Schema示例
const DepartmentSchema = z.object({
  id: z.string().openapi({
    description: "部门ID",
    example: "DEP001"
  }),
  name: z.string().openapi({
    description: "部门名称",
    example: "技术部"
  }),
  parent: z.object({
    id: z.string().openapi({ description: "父部门ID" })
  }).optional()
}).openapi({ description: "部门信息" });

现有makeSchemaDeepNullable工具函数

import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

extendZodWithOpenApi(z);

export function makeSchemaDeepNullable<T extends z.ZodTypeAny>(schema: T): any {
  // ZodObject - Use the type-preserving approach
  if (schema instanceof z.ZodObject) {
    const entries = Object.entries(schema.shape) as [string, z.ZodTypeAny][];
    
    const nullableShape = entries.reduce((acc, [key, value]) => {
      // Recursively make nested schemas nullable
      acc[key] = makeSchemaDeepNullable(value).nullable();
      return acc;
    }, {} as Record<string, z.ZodTypeAny>);
    
    return z.object(nullableShape).strict();
  }
  
  // ZodArray
  if (schema instanceof z.ZodArray) {
    const element = (schema as any).element as z.ZodTypeAny;
    return z.array(makeSchemaDeepNullable(element)).nullable();
  }
  
  // ZodUnion
  if (schema instanceof z.ZodUnion) {
    const options = ((schema as any).options as z.ZodTypeAny[]).map(opt => 
      makeSchemaDeepNullable(opt)
    );
    return z.union(options as any).nullable();
  }
  
  // ZodOptional
  if (schema instanceof z.ZodOptional) {
    const innerType = (schema as any)._def.innerType as z.ZodTypeAny;
    return makeSchemaDeepNullable(innerType).nullable().optional();
  }
  
  // ZodNullable
  if (schema instanceof z.ZodNullable) {
    const innerType = (schema as any)._def.innerType as z.ZodTypeAny;
    return makeSchemaDeepNullable(innerType).nullable();
  }
  
  // ZodRecord
  if (schema instanceof z.ZodRecord) {
    const valueType = (schema as any)._def.valueType as z.ZodTypeAny;
    const keyType = (schema as any)._def.keyType;
    return z.record(keyType, makeSchemaDeepNullable(valueType)).nullable();
  }
  
  // ZodDefault
  if (schema instanceof z.ZodDefault) {
    const innerType = (schema as any)._def.innerType as z.ZodTypeAny;
    return makeSchemaDeepNullable(innerType).nullable().optional();
  }
  
  // Primitives and others - directly call .nullable() to preserve metadata
  return schema.nullable();
}

响应Schema的使用方式

export const CreateDepartmentResponseSchema = makeSchemaDeepNullable(DepartmentSchema).openapi({
  description: 'Created department details'
}) as z.ZodObject<any>;

// OpenAPI响应定义
responses: {
  201: {
    description: "Department created successfully",
    content: {
      "application/json": {
        schema: CreateDepartmentResponseSchema,
      },
    },
  },
}

预期行为

生成的OpenAPI Schema应保留原始DepartmentSchema中所有字段的描述、示例等元数据,同时所有字段转为可空类型。

实际行为

生成的文档中字段确实显示为可空,但所有字段级的描述、示例元数据全部丢失,仅顶层的“Created department details”描述得以保留。

解决方案

问题根源在于:调用z.object()、z.array()等方法创建新Schema时,原始Schema的OpenAPI元数据不会自动继承,需要手动提取并复用这些元数据。

修改后的makeSchemaDeepNullable函数

核心逻辑:对每个Schema类型,先提取原始的OpenAPI元数据(存储在schema._def.openapi中),在创建新Schema后调用.openapi()方法将元数据传递过去。

import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

extendZodWithOpenApi(z);

// 扩展类型,用于访问Zod Schema的OpenAPI元数据
type ZodWithOpenApi = z.ZodTypeAny & {
  _def: {
    openapi?: Record<string, any>;
  };
};

export function makeSchemaDeepNullable<T extends z.ZodTypeAny>(schema: T): T {
  // 提取当前Schema的OpenAPI元数据
  const openapiMeta = (schema as ZodWithOpenApi)._def.openapi;

  // ZodObject处理
  if (schema instanceof z.ZodObject) {
    const entries = Object.entries(schema.shape) as [string, z.ZodTypeAny][];
    
    const nullableShape = entries.reduce((acc, [key, value]) => {
      acc[key] = makeSchemaDeepNullable(value);
      return acc;
    }, {} as Record<string, z.ZodTypeAny>);
    
    // 创建新Schema后复用原始元数据
    const newSchema = z.object(nullableShape).strict();
    return openapiMeta ? (newSchema.openapi(openapiMeta) as T) : (newSchema as T);
  }
  
  // ZodArray处理
  if (schema instanceof z.ZodArray) {
    const element = (schema as any).element as z.ZodTypeAny;
    const newSchema = z.array(makeSchemaDeepNullable(element)).nullable();
    return openapiMeta ? (newSchema.openapi(openapiMeta) as T) : (newSchema as T);
  }
  
  // ZodUnion处理
  if (schema instanceof z.ZodUnion) {
    const options = ((schema as any).options as z.ZodTypeAny[]).map(opt => 
      makeSchemaDeepNullable(opt)
    );
    const newSchema = z.union(options as any).nullable();
    return openapiMeta ? (newSchema.openapi(openapiMeta) as T) : (newSchema as T);
  }
  
  // ZodOptional处理
  if (schema instanceof z.ZodOptional) {
    const innerType = (schema as any)._def.innerType as z.ZodTypeAny;
    const newSchema = makeSchemaDeepNullable(innerType).nullable().optional();
    return openapiMeta ? (newSchema.openapi(openapiMeta) as T) : (newSchema as T);
  }
  
  // ZodNullable处理
  if (schema instanceof z.ZodNullable) {
    const innerType = (schema as any)._def.innerType as z.ZodTypeAny;
    const newSchema = makeSchemaDeepNullable(innerType).nullable();
    return openapiMeta ? (newSchema.openapi(openapiMeta) as T) : (newSchema as T);
  }
  
  // ZodRecord处理
  if (schema instanceof z.ZodRecord) {
    const valueType = (schema as any)._def.valueType as z.ZodTypeAny;
    const keyType = (schema as any)._def.keyType;
    const newSchema = z.record(keyType, makeSchemaDeepNullable(valueType)).nullable();
    return openapiMeta ? (newSchema.openapi(openapiMeta) as T) : (newSchema as T);
  }
  
  // ZodDefault处理
  if (schema instanceof z.ZodDefault) {
    const innerType = (schema as any)._def.innerType as z.ZodTypeAny;
    const newSchema = makeSchemaDeepNullable(innerType).nullable().optional();
    return openapiMeta ? (newSchema.openapi(openapiMeta) as T) : (newSchema as T);
  }
  
  // 基础类型处理:调用nullable后复用元数据
  const newSchema = schema.nullable();
  return openapiMeta ? (newSchema.openapi(openapiMeta) as T) : (newSchema as T);
}

关键修改点说明

  1. 新增ZodWithOpenApi类型,用于访问@asteasolutions/zod-to-openapi为Zod Schema添加的_def.openapi元数据字段。
  2. 每个类型分支中,创建新Schema后检查是否存在原始元数据,若存在则调用.openapi(openapiMeta)将元数据附加到新Schema上。
  3. 修正ZodObject分支逻辑:移除递归后的重复.nullable()调用,统一在各分支中处理可空转换,避免重复操作。

验证效果

修改后,调用makeSchemaDeepNullable(DepartmentSchema)生成的Schema会保留所有原始字段的描述、示例等元数据,同时所有字段转为可空类型,顶层的.openapi()调用也能正常叠加元数据。


内容的提问来源于stack exchange,提问作者The witcher

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 23:14:51