diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java index 482c286927c7..0071e15e1019 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java @@ -33,6 +33,7 @@ import org.openapitools.codegen.meta.features.SecurityFeature; import org.openapitools.codegen.model.ModelMap; import org.openapitools.codegen.model.ModelsMap; +import org.openapitools.codegen.model.OperationMap; import org.openapitools.codegen.model.OperationsMap; import org.openapitools.codegen.templating.mustache.IndentedLambda; import org.openapitools.codegen.utils.ModelUtils; @@ -711,6 +712,32 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L supportingFiles.add(new SupportingFile("models.index.mustache", modelPackage().replace('.', File.separatorChar), "index.ts")); } + // Convert operations returning "Null" (from OpenAPI 3.1 `type: 'null'`) to void. + // No Null model file is generated, so importing it would cause compilation errors. + OperationMap operationMap = operations.getOperations(); + if (operationMap != null) { + boolean hasNullReturnType = false; + for (CodegenOperation op : operationMap.getOperation()) { + if ("Null".equals(op.returnType)) { + op.returnType = null; + op.returnBaseType = null; + op.imports.remove("Null"); + hasNullReturnType = true; + } + } + if (hasNullReturnType) { + boolean anyOpStillImportsNull = operationMap.getOperation().stream() + .anyMatch(op -> op.imports.contains("Null")); + if (!anyOpStillImportsNull) { + List> imports = operations.getImports(); + imports.removeIf(im -> { + String importValue = im.get("import"); + return importValue != null && importValue.endsWith(".Null"); + }); + } + } + } + this.addOperationModelImportInformation(operations); this.escapeOperationIds(operations); this.updateOperationParameterForEnum(operations); @@ -813,12 +840,21 @@ private ExtendedCodegenModel processCodeGenModel(ExtendedCodegenModel cm) { .map(CodegenComposedSchemas::getOneOf) .orElse(Collections.emptyList()); + // Remove "Null" from oneOf variants. In OpenAPI 3.1, oneOf can include + // `type: 'null'` to represent nullable types. The codegen maps this to a + // "Null" model name, but no Null model file is generated, causing import + // errors. Instead, mark the model as nullable and filter out the Null entry. + if (cm.oneOf != null && !cm.oneOf.isEmpty() && cm.oneOf.remove("Null")) { + cm.isNullable = true; + } + // create a set of any non-primitive, non-array types used in the oneOf schemas which will // need to be imported. cm.oneOfModels = oneOfsList.stream() .filter(cp -> !cp.getIsPrimitiveType() && !cp.getIsArray()) .map(CodegenProperty::getBaseType) .filter(Objects::nonNull) + .filter(baseType -> !"Null".equals(baseType)) .collect(Collectors.toCollection(TreeSet::new)); // create a set of any complex, inner types used by arrays in the oneOf schema (e.g. if diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOfInterfaces.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOfInterfaces.mustache index ee4d1fccd8eb..3bb93d22917e 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOfInterfaces.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOfInterfaces.mustache @@ -3,4 +3,4 @@ * {{#lambda.indented_star_1}}{{{unescapedDescription}}}{{/lambda.indented_star_1}} * @export */ -export type {{classname}} = {{#discriminator}}{{#mappedModels}}{ {{discriminator.propertyName}}: '{{mappingName}}' } & {{modelName}}{{^-last}} | {{/-last}}{{/mappedModels}}{{/discriminator}}{{^discriminator}}{{#oneOf}}{{{.}}}{{^-last}} | {{/-last}}{{/oneOf}}{{/discriminator}}; \ No newline at end of file +export type {{classname}} = {{#discriminator}}{{#mappedModels}}{ {{discriminator.propertyName}}: '{{mappingName}}' } & {{modelName}}{{^-last}} | {{/-last}}{{/mappedModels}}{{/discriminator}}{{^discriminator}}{{#oneOf}}{{{.}}}{{^-last}} | {{/-last}}{{/oneOf}}{{/discriminator}}{{#isNullable}} | null{{/isNullable}}; \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java index da8324317640..0a1eb8791b33 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java @@ -444,6 +444,60 @@ public void testOneOfModelsImportNonPrimitiveTypes() throws IOException { TestUtils.assertFileContains(testResponse, "import type { OptionThree } from './OptionThree'"); } + @Test(description = "Verify nullable oneOf does not generate Null model references") + public void testNullableOneOfDoesNotImportNullModel() throws IOException { + File output = generate( + Collections.emptyMap(), + "src/test/resources/3_0/typescript-fetch/nullable-oneOf.yaml" + ); + + Path nullableResult = Paths.get(output + "/models/NullableResult.ts"); + TestUtils.assertFileExists(nullableResult); + + // Should not import or reference a non-existent "Null" model + TestUtils.assertFileNotContains(nullableResult, "import type { Null }"); + TestUtils.assertFileNotContains(nullableResult, "NullFromJSON"); + TestUtils.assertFileNotContains(nullableResult, "NullToJSON"); + TestUtils.assertFileNotContains(nullableResult, "instanceOfNull"); + // Should contain the valid model types + TestUtils.assertFileContains(nullableResult, "FileLocation"); + TestUtils.assertFileContains(nullableResult, "DetailedLocation"); + // Union type should not include Null but should be nullable + TestUtils.assertFileContains(nullableResult, "export type NullableResult = DetailedLocation | FileLocation | null"); + + // No Null.ts model file should be generated + TestUtils.assertFileNotExists(Paths.get(output + "/models/Null.ts")); + } + + @Test(description = "Verify null response type is converted to void") + public void testNullResponseTypeConvertedToVoid() throws IOException { + File output = generate( + Collections.emptyMap(), + "src/test/resources/3_0/typescript-fetch/null-response.yaml" + ); + + Path apiFile = Paths.get(output + "/apis/ItemsApi.ts"); + TestUtils.assertFileExists(apiFile); + + // Should not import or reference a "Null" model + TestUtils.assertFileNotContains(apiFile, "Null,"); + TestUtils.assertFileNotContains(apiFile, "NullFromJSON"); + TestUtils.assertFileNotContains(apiFile, "NullToJSON"); + + // Delete endpoint should use void return type + TestUtils.assertFileContains(apiFile, "Promise>"); + TestUtils.assertFileContains(apiFile, "VoidApiResponse"); + + // Get endpoint should still use Item model + TestUtils.assertFileContains(apiFile, "ItemFromJSON"); + TestUtils.assertFileContains(apiFile, "Promise>"); + + // No Null.ts model should be generated + TestUtils.assertFileNotExists(Paths.get(output + "/models/Null.ts")); + // Item model should still exist + TestUtils.assertFileExists(Paths.get(output + "/models/Item.ts")); + } + @Test(description = "Verify validationAttributes works with withoutRuntimeChecks=true") public void testValidationAttributesWithWithoutRuntimeChecks() throws IOException { Map properties = new HashMap<>(); diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/null-response.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/null-response.yaml new file mode 100644 index 000000000000..778cddb827cf --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/null-response.yaml @@ -0,0 +1,54 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: testing null response type conversion to void +paths: + /items/{itemId}: + delete: + tags: + - items + operationId: deleteItem + summary: Delete an item + parameters: + - name: itemId + in: path + required: true + schema: + type: string + responses: + '200': + description: Item deleted successfully + content: + application/json: + schema: + type: 'null' + get: + tags: + - items + operationId: getItem + summary: Get an item by ID + parameters: + - name: itemId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/nullable-oneOf.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/nullable-oneOf.yaml new file mode 100644 index 000000000000..77b5fbde0b56 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/nullable-oneOf.yaml @@ -0,0 +1,45 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: testing nullable oneOf (Null type filtering) +paths: + /locations/{locationId}: + get: + operationId: getLocation + parameters: + - name: locationId + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/NullableResult' +components: + schemas: + NullableResult: + oneOf: + - $ref: '#/components/schemas/FileLocation' + - $ref: '#/components/schemas/DetailedLocation' + - type: 'null' + FileLocation: + type: object + properties: + path: + type: string + required: + - path + DetailedLocation: + type: object + properties: + path: + type: string + size: + type: integer + required: + - path + - size