orval Error: Duplicate schema names detected

一、报错现场

后端使用tsoa生成了openApi文档,
前端再通过执行 orval 生成代码ts类型时,终端突然抛出:

Error: Duplicate schema names detected

紧接着生成便中断,目录里只残留了部分 .ts 文件。
经过排查定位到:

  • 两个 Controller 的方法名都叫 get
  • orval 试图分别写出 getParams.ts,第二份文件直接冲突。
// UserController.ts
@Route('api/user')
@Tags('user')
export class UserController extends Controller {
  @Get()
  public async get(@Query() userId: string): ApiResponse<any> {
    return JsonResult.success();
  }
}

// RepoController.ts
@Route('api/repo')
@Tags('repo')
export class RepoController extends Controller {
  @Get()
  public async get(@Query() repo: string): ApiResponse<any> {
    return JsonResult.success();
  }
}

二、根因分析

  1. OpenAPI 规范规定:所有 schema 名必须 全局唯一
  2. tsoa 默认使用 方法名 + Params/Body/Response 作为 schema 名,Controller 名不参与。
  3. orval 于是按照上诉产出的 OpenAPI 文档生成代码。

因此两个 get 的参数结构都被命名为 getParams,同名即冲突。


三、解决方案

1. 显式指定 operationId(推荐)

给冲突的方法补上语义化的 @OperationId

@Get()
@OperationId('user_get')   // 生成 user_getParams
public async get(@Query() userId: string) { ... }

@Get()
@OperationId('repo_get')   // 生成 repo_getParams
public async get(@Query() repo: string) { ... }

2. 直接重命名方法(简单暴力)

public async getUser(@Query() userId: string) { ... }
public async getRepo(@Query() repo: string) { ... }

四、一句话总结

tsoa 不会依据 Controller 会自动成为命名空间 ,当 orval 抛出 “Error: Duplicate schema names detected” 时,用 @OperationId 或重命名方法即可;

OpenAPI 规范本身并没有“Controller”这一概念;它只有 paths、operations、schemas 等层级。
之所以最终出现同名 schema,是因为 tsoa 在把 TypeScript 代码转换成 OpenAPI 文档时,没有把 Controller 名(或任何等价前缀)纳入到生成的 schema title 或 operationId 中,导致不同 Controller 里的同名方法生成了同样的模型名(如 getParams)。
orval 只是忠实消费这份 OpenAPI 文件,于是看到两个完全同名的 schema 就报冲突(即operationIds重复,也就是paths>/api/user>get的operationId和paths>/api/repo>get的operationId一样)。
所以根因确实在 tsoa 的默认命名策略,我在orval和 tsoa 分别给官方提了issueissue ,看看官方是否有合适我需求的方案!

五:最佳方案

更改tsoa生成operationId规则

更改 tsoa 生成operationId的规则,不再是单单以方法名为id,而是结合控制器名

tsoa.json

{
  "spec": {
    "operationIdTemplate": "{{controllerName}}-{{titleCase method.name}}",
  }
}

再次执行前端生成,orval 生成的代码(接口和模型)都带上前缀,如

endpoints>user.ts>UserController-Get
model>userControllerGetParam.ts>userControllerGetParam

优化生成代码命名

其实我们最终想要的是如下:
endpoints无需添加命名空间,因为我外层有自己做 比如 api.user.get.
model是需要添加命名空间,但是 userControllerGetParam 太丑了,需要改为 userGetParam
最终目标如下

endpoints>user.ts>get
model>userGetParam.ts>userGetParam

接下来我们一步步实现:
通过orval>operationName去除前缀operationId里前缀中的Controller字符

// defineConfig>petstore>output>override
operationName: (operation, route, verb) => {
  const operationId = operation.operationId;
  if (!operationId) {
    // 如果没有 operationId,使用默认逻辑生成
    return `${verb}${route.replace(/[{}]/g, '').replace(/\//g, '')}`;
  }
  // 去掉 Controller 字段
  // 例如:'userController-get' -> 'user-get'
  // 例如:'userControllerGet' -> 'userGet'
  const newOperationId = operationId.replace(/Controller/g, '');
  return newOperationId;
},

如上endpoints和model的前缀(既operationId里)中的Controller字符就都删除了,
比如:
以前的 endpoints>user.ts>UserController-GetList已经改为了 User-GetList
以前的 model>userControllerGetParam.ts>userControllerGetParam已经改为了 userGetParam

接下来我们再将endpoints的 User-GetList 进一步改为 getList即可,

// defineConfig>petstore>output>transformer
transformer(operation) {
  // 1. 修改方法名:添加 Abc 后缀
  if (operation && operation.operationName) {
    const originalName = operation.operationName;
    // 处理带连字符的格式, 如:Me-Update -> update
    if (originalName.includes('-')) {
      const parts = originalName.split('-');
      if (parts.length >= 2) {
        const action = parts[parts.length - 1]; // 取最后一部分
        operation.operationName = action.charAt(0).toLowerCase() + action.slice(1); // 首字母小写
      }
    }
  }
  return operation;
}

再次执行生成,就好了!

🤪: 不知道为什么 override>operationName作用的是全局(模型和方法),而 override>transformer修改的originalName却仅只会作用到模型上,不过我们正好利用这个规则完成了我们的需求!

posted @ 2025-08-19 00:26  丁少华  阅读(29)  评论(0)    收藏  举报