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();
}
}
二、根因分析
- OpenAPI 规范规定:所有 schema 名必须 全局唯一。
- tsoa 默认使用
方法名 + Params/Body/Response作为 schema 名,Controller 名不参与。 - 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 分别给官方提了issue、 issue ,看看官方是否有合适我需求的方案!
五:最佳方案
更改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却仅只会作用到模型上,不过我们正好利用这个规则完成了我们的需求!

浙公网安备 33010602011771号