Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Map<String, String>> 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);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}};
export type {{classname}} = {{#discriminator}}{{#mappedModels}}{ {{discriminator.propertyName}}: '{{mappingName}}' } & {{modelName}}{{^-last}} | {{/-last}}{{/mappedModels}}{{/discriminator}}{{^discriminator}}{{#oneOf}}{{{.}}}{{^-last}} | {{/-last}}{{/oneOf}}{{/discriminator}}{{#isNullable}} | null{{/isNullable}};
Original file line number Diff line number Diff line change
Expand Up @@ -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<runtime.ApiResponse<void>>");
TestUtils.assertFileContains(apiFile, "VoidApiResponse");

// Get endpoint should still use Item model
TestUtils.assertFileContains(apiFile, "ItemFromJSON");
TestUtils.assertFileContains(apiFile, "Promise<runtime.ApiResponse<Item>>");

// 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<String, Object> properties = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading