From 1c20f98bf9e09385ba3b56ffafdc749d50bb602e Mon Sep 17 00:00:00 2001 From: BambooStrop Date: Wed, 14 Jan 2026 15:08:38 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20OpenAPI=203.1?= =?UTF-8?q?=20type=20=E6=95=B0=E7=BB=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持 OpenAPI 3.1 规范中的 type 数组格式(如 ["string", "null"]) - 更新类型定义以支持 type 为数组的情况 - 在 getDefaultType 函数中添加 type 数组处理逻辑 - 在 resolveEnumObject 函数中添加 type 数组处理逻辑 - 在 primitive 函数中添加 type 数组处理逻辑 - 添加测试用例验证 OpenAPI 3.1 type 数组格式 修复 issue: 当使用 OpenAPI 3.1 规范时,type 字段为数组格式会导致生成的类型为 unknown --- src/generator/serviceGeneratorHelper.ts | 23 +++++++- src/generator/util.ts | 14 +++++ src/parser-mock/index.ts | 12 +++- src/type.ts | 3 +- ...17 (\345\246\202 [_string_, _null_]).snap" | 58 +++++++++++++++++++ test/openapi-3.1-type-array.json | 47 +++++++++++++++ test/openapi-3.1-type-array.spec.ts | 19 ++++++ 7 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 "test/__snapshots__/openapi-3.1-type-array/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" create mode 100644 test/openapi-3.1-type-array.json create mode 100644 test/openapi-3.1-type-array.spec.ts diff --git a/src/generator/serviceGeneratorHelper.ts b/src/generator/serviceGeneratorHelper.ts index 386cc2f..b0161ce 100644 --- a/src/generator/serviceGeneratorHelper.ts +++ b/src/generator/serviceGeneratorHelper.ts @@ -112,7 +112,18 @@ export function resolveEnumObject(params: { let enumStr = ''; let enumLabelTypeStr = ''; - if (numberEnum.includes(schemaObject.type) || isAllNumber(enumArray)) { + // 获取实际的类型(处理 OpenAPI 3.1 的 type 数组情况) + const getActualType = (type: typeof schemaObject.type): string => { + if (Array.isArray(type)) { + // 如果是数组,返回第一个非 null 类型 + return type.find((t) => t !== 'null') || 'string'; + } + return type; + }; + + const actualType = getActualType(schemaObject.type); + + if (numberEnum.includes(actualType) || isAllNumber(enumArray)) { if (config.isSupportParseEnumDesc && schemaObject.description) { const enumMap = parseDescriptionEnum(schemaObject.description); enumStr = `{${map(enumArray, (value) => { @@ -151,7 +162,7 @@ export function resolveEnumObject(params: { return `${value}:"${enumLabel}"`; }).join(',')}}`; } else { - if (numberEnum.includes(schemaObject.type) || isAllNumber(enumArray)) { + if (numberEnum.includes(actualType) || isAllNumber(enumArray)) { if ( (config.isSupportParseEnumDesc || config.supportParseEnumDescByReg) && schemaObject.description @@ -334,7 +345,13 @@ export function resolveRefObject(params: { resolvedType = (refResolved as { type?: string })?.type; } else { const schemaObj: SchemaObject = schema; - resolvedType = schemaObj.type; + // 处理 OpenAPI 3.1 的 type 数组情况 + if (Array.isArray(schemaObj.type)) { + // 如果是数组,使用第一个非 null 类型 + resolvedType = schemaObj.type.find((t) => t !== 'null') || 'string'; + } else { + resolvedType = schemaObj.type; + } } const finalSchema = schema as SchemaObject; diff --git a/src/generator/util.ts b/src/generator/util.ts index 9db87b2..f0bc9c5 100644 --- a/src/generator/util.ts +++ b/src/generator/util.ts @@ -144,6 +144,20 @@ export function getDefaultType( const dateEnum = ['Date', 'date', 'dateTime', 'date-time', 'datetime']; const stringEnum = ['string', 'email', 'password', 'url', 'byte', 'binary']; + // OpenAPI 3.1 支持 type 为数组,例如 ["string", "null"] + if (Array.isArray(type)) { + return type + .map((t) => { + // 为数组中的每个类型创建一个临时的 schemaObject + const tempSchema: ISchemaObject = { + ...schemaObject, + type: t, + }; + return getDefaultType(tempSchema, namespace, schemas); + }) + .join(' | '); + } + if (type === 'null') { return 'null'; } diff --git a/src/parser-mock/index.ts b/src/parser-mock/index.ts index 32ad4f7..f0be344 100644 --- a/src/parser-mock/index.ts +++ b/src/parser-mock/index.ts @@ -132,12 +132,18 @@ function primitive( ) { const schema = objectify(schemaParams); const { type, format } = schema; + + // 处理 OpenAPI 3.1 的 type 数组情况 + const actualType = Array.isArray(type) + ? type.find((t) => t !== 'null') || 'string' + : type; + const value = - primitives[`${type}_${format || getDateByName(propsName)}`] || - primitives[type]; + primitives[`${actualType}_${format || getDateByName(propsName)}`] || + primitives[actualType]; if (isUndefined(schema.example)) { - return value || `Unknown Type: ${schema.type}`; + return value || `Unknown Type: ${actualType}`; } return schema.example as string; diff --git a/src/type.ts b/src/type.ts index 0084561..d3d1a87 100644 --- a/src/type.ts +++ b/src/type.ts @@ -18,7 +18,8 @@ export type MutuallyExclusiveWithFallback = { type Modify = Omit & R; type ICustomBaseSchemaObject = { - type: ISchemaObjectType; + // OpenAPI 3.1 支持 type 为数组,例如 ["string", "null"] + type: ISchemaObjectType | ISchemaObjectType[]; format?: ISchemaObjectFormat; additionalProperties?: boolean | ISchemaObject; properties?: { diff --git "a/test/__snapshots__/openapi-3.1-type-array/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" "b/test/__snapshots__/openapi-3.1-type-array/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" new file mode 100644 index 0000000..86bb4f8 --- /dev/null +++ "b/test/__snapshots__/openapi-3.1-type-array/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" @@ -0,0 +1,58 @@ +/* eslint-disable */ +// @ts-ignore +import * as API from './types'; + +export function displayUser(field: keyof API.User) { + return { + idCard: '身份证号', + name: '姓名', + age: '年龄', + isActive: '是否激活', + tags: '标签', + }[field]; +} +/* eslint-disable */ +// @ts-ignore +import request from 'axios'; + +import * as API from './types'; + +/** Get user GET /user */ +export function userUsingGet({ + options, +}: { + options?: { [key: string]: unknown }; +}) { + return request('/user', { + method: 'GET', + ...(options || {}), + }); +} +/* eslint-disable */ +// @ts-ignore +export * from './types'; +export * from './displayTypeLabel'; + +export * from './getUser'; +/* eslint-disable */ +// @ts-ignore + +export type User = { + /** 身份证号 */ + idCard?: string | null; + /** 姓名 */ + name?: string | null; + /** 年龄 */ + age?: number | null; + /** 是否激活 */ + isActive?: boolean | null; + /** 标签 */ + tags?: string[] | null; +}; + +export type UserUsingGetResponses = { + /** + * Success + */ + 200: User; +}; diff --git a/test/openapi-3.1-type-array.json b/test/openapi-3.1-type-array.json new file mode 100644 index 0000000..8a1f834 --- /dev/null +++ b/test/openapi-3.1-type-array.json @@ -0,0 +1,47 @@ +{ + "openapi": "3.1.0", + "info": { "title": "OpenAPI 3.1 Type Array Test", "version": "1.0.0" }, + "paths": { + "/user": { + "get": { + "summary": "Get user", + "operationId": "getUser", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/User" } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "idCard": { + "type": ["string", "null"], + "description": "身份证号", + "example": "110101199001011234" + }, + "name": { "type": ["string", "null"], "description": "姓名" }, + "age": { "type": ["integer", "null"], "description": "年龄" }, + "isActive": { + "type": ["boolean", "null"], + "description": "是否激活" + }, + "tags": { + "type": ["array", "null"], + "items": { "type": "string" }, + "description": "标签" + } + } + } + } + } +} diff --git a/test/openapi-3.1-type-array.spec.ts b/test/openapi-3.1-type-array.spec.ts new file mode 100644 index 0000000..0a5a733 --- /dev/null +++ b/test/openapi-3.1-type-array.spec.ts @@ -0,0 +1,19 @@ +import { join } from 'path'; +import { describe, expect, it } from 'vitest'; + +import * as openAPI from '../src/index'; +import { getSnapshotDir, readGeneratedFiles } from './testUtils'; + +describe('OpenAPI 3.1 Type Array Support', () => { + it('测试 OpenAPI 3.1 type 数组格式 (如 ["string", "null"])', async (ctx) => { + await openAPI.generateService({ + schemaPath: join(import.meta.dirname, './openapi-3.1-type-array.json'), + serversPath: './apis/openapi-3.1-type-array', + isDisplayTypeLabel: true, + }); + + await expect( + readGeneratedFiles('./apis/openapi-3.1-type-array') + ).resolves.toMatchFileSnapshot(getSnapshotDir(ctx)); + }); +}); From 061df4e4ee4cae8ac1cb8c9a10394a870e9f9b4f Mon Sep 17 00:00:00 2001 From: BambooStrop Date: Wed, 14 Jan 2026 15:18:57 +0800 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20changeset=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/openapi-3-1-type-array-support.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/openapi-3-1-type-array-support.json diff --git a/.changeset/openapi-3-1-type-array-support.json b/.changeset/openapi-3-1-type-array-support.json new file mode 100644 index 0000000..0036d8f --- /dev/null +++ b/.changeset/openapi-3-1-type-array-support.json @@ -0,0 +1,4 @@ +{ + "type": "minor", + "summary": "添加 OpenAPI 3.1 type 数组支持,支持 type 字段为数组格式(如 [\"string\", \"null\"])" +} From cfd42c6afffe46d86ea08dd80b4db3af2cf37462 Mon Sep 17 00:00:00 2001 From: BambooStrop Date: Wed, 14 Jan 2026 15:46:44 +0800 Subject: [PATCH 3/6] =?UTF-8?q?chore:=20=E4=BF=AE=E5=A4=8D=20changeset=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=B9=B6=E4=BC=98=E5=8C=96=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将测试用例从独立文件合并到 common.spec.ts - 修复 changeset 文件格式(.json → .md) - 使用 patch 版本级别(1.12.3) - 更新 CHANGELOG --- .../openapi-3-1-type-array-support.json | 4 -- CHANGELOG.md | 6 ++ package.json | 2 +- ...17 (\345\246\202 [_string_, _null_]).snap" | 58 +++++++++++++++++++ test/common.spec.ts | 12 ++++ test/openapi-3.1-type-array.spec.ts | 19 ------ 6 files changed, 77 insertions(+), 24 deletions(-) delete mode 100644 .changeset/openapi-3-1-type-array-support.json create mode 100644 "test/__snapshots__/common/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" delete mode 100644 test/openapi-3.1-type-array.spec.ts diff --git a/.changeset/openapi-3-1-type-array-support.json b/.changeset/openapi-3-1-type-array-support.json deleted file mode 100644 index 0036d8f..0000000 --- a/.changeset/openapi-3-1-type-array-support.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "minor", - "summary": "添加 OpenAPI 3.1 type 数组支持,支持 type 字段为数组格式(如 [\"string\", \"null\"])" -} diff --git a/CHANGELOG.md b/CHANGELOG.md index a762bd1..bed3d8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # openapi-ts-request +## 1.12.3 + +### Patch Changes + +- 添加 OpenAPI 3.1 type 数组支持,支持 type 字段为数组格式(如 ["string", "null"]) + ## 1.12.2 ### Patch Changes diff --git a/package.json b/package.json index 316c154..bfdf18f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-ts-request", - "version": "1.12.2", + "version": "1.12.3", "description": "Swagger2/OpenAPI3/Apifox to TypeScript/JavaScript, request client(support any client), request mock service, enum and enum translation, react-query/vue-query, type field label, JSON Schemas", "packageManager": "pnpm@9.15.0", "engines": { diff --git "a/test/__snapshots__/common/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" "b/test/__snapshots__/common/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" new file mode 100644 index 0000000..86bb4f8 --- /dev/null +++ "b/test/__snapshots__/common/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" @@ -0,0 +1,58 @@ +/* eslint-disable */ +// @ts-ignore +import * as API from './types'; + +export function displayUser(field: keyof API.User) { + return { + idCard: '身份证号', + name: '姓名', + age: '年龄', + isActive: '是否激活', + tags: '标签', + }[field]; +} +/* eslint-disable */ +// @ts-ignore +import request from 'axios'; + +import * as API from './types'; + +/** Get user GET /user */ +export function userUsingGet({ + options, +}: { + options?: { [key: string]: unknown }; +}) { + return request('/user', { + method: 'GET', + ...(options || {}), + }); +} +/* eslint-disable */ +// @ts-ignore +export * from './types'; +export * from './displayTypeLabel'; + +export * from './getUser'; +/* eslint-disable */ +// @ts-ignore + +export type User = { + /** 身份证号 */ + idCard?: string | null; + /** 姓名 */ + name?: string | null; + /** 年龄 */ + age?: number | null; + /** 是否激活 */ + isActive?: boolean | null; + /** 标签 */ + tags?: string[] | null; +}; + +export type UserUsingGetResponses = { + /** + * Success + */ + 200: User; +}; diff --git a/test/common.spec.ts b/test/common.spec.ts index 0c93dec..031b8bb 100644 --- a/test/common.spec.ts +++ b/test/common.spec.ts @@ -540,4 +540,16 @@ export async function ${api.functionName}(${api.body ? `data: ${api.body.type}` readGeneratedFiles('./apis/split-types-by-module') ).resolves.toMatchFileSnapshot(getSnapshotDir(ctx)); }); + + it('测试 OpenAPI 3.1 type 数组格式 (如 ["string", "null"])', async (ctx) => { + await openAPI.generateService({ + schemaPath: join(import.meta.dirname, './openapi-3.1-type-array.json'), + serversPath: './apis/openapi-3.1-type-array', + isDisplayTypeLabel: true, + }); + + await expect( + readGeneratedFiles('./apis/openapi-3.1-type-array') + ).resolves.toMatchFileSnapshot(getSnapshotDir(ctx)); + }); }); diff --git a/test/openapi-3.1-type-array.spec.ts b/test/openapi-3.1-type-array.spec.ts deleted file mode 100644 index 0a5a733..0000000 --- a/test/openapi-3.1-type-array.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { join } from 'path'; -import { describe, expect, it } from 'vitest'; - -import * as openAPI from '../src/index'; -import { getSnapshotDir, readGeneratedFiles } from './testUtils'; - -describe('OpenAPI 3.1 Type Array Support', () => { - it('测试 OpenAPI 3.1 type 数组格式 (如 ["string", "null"])', async (ctx) => { - await openAPI.generateService({ - schemaPath: join(import.meta.dirname, './openapi-3.1-type-array.json'), - serversPath: './apis/openapi-3.1-type-array', - isDisplayTypeLabel: true, - }); - - await expect( - readGeneratedFiles('./apis/openapi-3.1-type-array') - ).resolves.toMatchFileSnapshot(getSnapshotDir(ctx)); - }); -}); From dfb96eb812e764f062aa4dceed358633e945a064 Mon Sep 17 00:00:00 2001 From: luochao <1055120207@qq.com> Date: Wed, 14 Jan 2026 15:55:55 +0800 Subject: [PATCH 4/6] docs: changelog --- .changeset/sunny-moons-appear.md | 5 +++++ CHANGELOG.md | 6 ------ package.json | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 .changeset/sunny-moons-appear.md diff --git a/.changeset/sunny-moons-appear.md b/.changeset/sunny-moons-appear.md new file mode 100644 index 0000000..049865a --- /dev/null +++ b/.changeset/sunny-moons-appear.md @@ -0,0 +1,5 @@ +--- +'openapi-ts-request': patch +--- + +perf: 添加 OpenAPI 3.1 type 数组支持,支持 type 字段为数组格式(如:["string", "null"]) diff --git a/CHANGELOG.md b/CHANGELOG.md index bed3d8d..a762bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,5 @@ # openapi-ts-request -## 1.12.3 - -### Patch Changes - -- 添加 OpenAPI 3.1 type 数组支持,支持 type 字段为数组格式(如 ["string", "null"]) - ## 1.12.2 ### Patch Changes diff --git a/package.json b/package.json index bfdf18f..316c154 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-ts-request", - "version": "1.12.3", + "version": "1.12.2", "description": "Swagger2/OpenAPI3/Apifox to TypeScript/JavaScript, request client(support any client), request mock service, enum and enum translation, react-query/vue-query, type field label, JSON Schemas", "packageManager": "pnpm@9.15.0", "engines": { From 7bb829fb0f396bd2e64f3f7f2bc3daeba2611ea8 Mon Sep 17 00:00:00 2001 From: luochao <1055120207@qq.com> Date: Wed, 14 Jan 2026 15:57:58 +0800 Subject: [PATCH 5/6] chore: clean code --- test/common.spec.ts | 5 ++++- test/{ => example-files}/openapi-3.1-type-array.json | 0 2 files changed, 4 insertions(+), 1 deletion(-) rename test/{ => example-files}/openapi-3.1-type-array.json (100%) diff --git a/test/common.spec.ts b/test/common.spec.ts index 031b8bb..93822fb 100644 --- a/test/common.spec.ts +++ b/test/common.spec.ts @@ -543,7 +543,10 @@ export async function ${api.functionName}(${api.body ? `data: ${api.body.type}` it('测试 OpenAPI 3.1 type 数组格式 (如 ["string", "null"])', async (ctx) => { await openAPI.generateService({ - schemaPath: join(import.meta.dirname, './openapi-3.1-type-array.json'), + schemaPath: join( + import.meta.dirname, + './example-files/openapi-3.1-type-array.json' + ), serversPath: './apis/openapi-3.1-type-array', isDisplayTypeLabel: true, }); diff --git a/test/openapi-3.1-type-array.json b/test/example-files/openapi-3.1-type-array.json similarity index 100% rename from test/openapi-3.1-type-array.json rename to test/example-files/openapi-3.1-type-array.json From 50f256118feb7847c5d626f43108f3cad389fc87 Mon Sep 17 00:00:00 2001 From: luochao <1055120207@qq.com> Date: Wed, 14 Jan 2026 16:01:02 +0800 Subject: [PATCH 6/6] chore: clean code --- ...17 (\345\246\202 [_string_, _null_]).snap" | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 "test/__snapshots__/openapi-3.1-type-array/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" diff --git "a/test/__snapshots__/openapi-3.1-type-array/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" "b/test/__snapshots__/openapi-3.1-type-array/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" deleted file mode 100644 index 86bb4f8..0000000 --- "a/test/__snapshots__/openapi-3.1-type-array/\346\265\213\350\257\225 OpenAPI 3.1 type \346\225\260\347\273\204\346\240\274\345\274\217 (\345\246\202 [_string_, _null_]).snap" +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable */ -// @ts-ignore -import * as API from './types'; - -export function displayUser(field: keyof API.User) { - return { - idCard: '身份证号', - name: '姓名', - age: '年龄', - isActive: '是否激活', - tags: '标签', - }[field]; -} -/* eslint-disable */ -// @ts-ignore -import request from 'axios'; - -import * as API from './types'; - -/** Get user GET /user */ -export function userUsingGet({ - options, -}: { - options?: { [key: string]: unknown }; -}) { - return request('/user', { - method: 'GET', - ...(options || {}), - }); -} -/* eslint-disable */ -// @ts-ignore -export * from './types'; -export * from './displayTypeLabel'; - -export * from './getUser'; -/* eslint-disable */ -// @ts-ignore - -export type User = { - /** 身份证号 */ - idCard?: string | null; - /** 姓名 */ - name?: string | null; - /** 年龄 */ - age?: number | null; - /** 是否激活 */ - isActive?: boolean | null; - /** 标签 */ - tags?: string[] | null; -}; - -export type UserUsingGetResponses = { - /** - * Success - */ - 200: User; -};