diff --git a/.changeset/popular-poems-tease.md b/.changeset/popular-poems-tease.md new file mode 100644 index 000000000..7e1cf6c8f --- /dev/null +++ b/.changeset/popular-poems-tease.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +"Fix handling of negative property names in TypeScript code generation" diff --git a/packages/openapi-ts-tests/test/3.1.x.test.ts b/packages/openapi-ts-tests/test/3.1.x.test.ts index 260537b8b..85a27f8e5 100644 --- a/packages/openapi-ts-tests/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/test/3.1.x.test.ts @@ -518,6 +518,13 @@ describe(`OpenAPI ${version}`, () => { description: 'does not set oneOf composition ref model properties as required', }, + { + config: createConfig({ + input: 'negative-property-names.json', + output: 'negative-property-names', + }), + description: 'handles negative property names correctly', + }, { config: createConfig({ input: 'schema-const.yaml', diff --git a/packages/openapi-ts-tests/test/__snapshots__/3.1.x/negative-property-names/index.ts b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/negative-property-names/index.ts new file mode 100644 index 000000000..56bade120 --- /dev/null +++ b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/negative-property-names/index.ts @@ -0,0 +1,2 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from './types.gen'; \ No newline at end of file diff --git a/packages/openapi-ts-tests/test/__snapshots__/3.1.x/negative-property-names/types.gen.ts b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/negative-property-names/types.gen.ts new file mode 100644 index 000000000..ffbb4f506 --- /dev/null +++ b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/negative-property-names/types.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ReactionRollup = { + url: string; + total_count: number; + '+1': number; + "-1": number; + laugh: number; + confused: number; + heart: number; + hooray: number; + eyes: number; + rocket: number; +}; + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; \ No newline at end of file diff --git a/packages/openapi-ts-tests/test/spec/3.1.x/negative-property-names.json b/packages/openapi-ts-tests/test/spec/3.1.x/negative-property-names.json new file mode 100644 index 000000000..52339b0f9 --- /dev/null +++ b/packages/openapi-ts-tests/test/spec/3.1.x/negative-property-names.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "OpenAPI 3.1.1 negative property names example", + "version": "1" + }, + "components": { + "schemas": { + "ReactionRollup": { + "title": "Reaction Rollup", + "type": "object", + "properties": { + "url": { + "format": "uri", + "type": "string" + }, + "total_count": { + "type": "integer" + }, + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "rocket": { + "type": "integer" + } + }, + "required": [ + "url", + "total_count", + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "eyes", + "rocket" + ] + } + } + } +} diff --git a/packages/openapi-ts/src/plugins/shared/utils/case.ts b/packages/openapi-ts/src/plugins/shared/utils/case.ts index 11337ea3e..7bdd24404 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/case.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/case.ts @@ -17,6 +17,10 @@ export const fieldName = ({ }) => { numberRegExp.lastIndex = 0; if (numberRegExp.test(name)) { + // For negative numbers, use string literals instead + if (name.startsWith('-')) { + return ts.factory.createStringLiteral(name); + } return ts.factory.createNumericLiteral(name); } diff --git a/packages/openapi-ts/src/plugins/zod/plugin.ts b/packages/openapi-ts/src/plugins/zod/plugin.ts index 960dca9ce..2e5630866 100644 --- a/packages/openapi-ts/src/plugins/zod/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/plugin.ts @@ -360,9 +360,16 @@ const objectTypeToZodSchema = ({ }); numberRegExp.lastIndex = 0; - let propertyName = numberRegExp.test(name) - ? ts.factory.createNumericLiteral(name) - : name; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = name; + } // TODO: parser - abstract safe property name logic if ( ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) &&