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 @@ -986,18 +986,13 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
} else if (ModelUtils.hasProperties(schema)) {
// OAS 3.1: if the type array includes "null", extract it and set nullable:true
// on the parent schema before normalizing its child properties.
// We intentionally do NOT call the full processNormalize31Spec here because
// that method can replace a JsonSchema with properties (but no explicit type)
// with an empty schema, discarding all properties.
if (getRule(NORMALIZE_31SPEC) && schema.getTypes() != null && schema.getTypes().contains("null")) {
schema.setNullable(true);
schema.getTypes().remove("null");
if (schema.getTypes().size() == 1) {
schema.setType(String.valueOf(schema.getTypes().iterator().next()));
}
}
normalizeNullTypeArray31(schema);
normalizeProperties(schema, visitedSchemas);
} else if (schema.getAdditionalProperties() instanceof Schema) { // map
// OAS 3.1: same as the schema-with-properties branch above, a map schema can
// declare its own nullability with a type array (e.g.
// `type: [object, "null"]` alongside `additionalProperties`).
normalizeNullTypeArray31(schema);
normalizeMapSchema(schema);
Schema additionalProperties = (Schema) schema.getAdditionalProperties();
if (getRule(NORMALIZE_31SPEC) && ModelUtils.isNullTypeSchema(openAPI, additionalProperties)) {
Expand Down Expand Up @@ -1108,6 +1103,30 @@ protected Schema normalizeMapSchema(Schema schema) {
return processSetMapToNullable(schema);
}

/**
* OAS 3.1 allows a schema to express nullability with a type array, e.g.
* `type: [object, "null"]`. Move the `null` entry onto `nullable: true` and, once a single
* type is left, set it as the OAS 3.0 style `type` so that the rest of the codegen sees a
* plain (nullable) object or map instead of a multi-type schema.
* <p>
* This is deliberately narrower than {@link #processNormalize31Spec(Schema, Set)}: that method
* replaces a JsonSchema carrying no explicit type with an empty schema, which would discard the
* `properties` / `additionalProperties` of the schemas handled here.
*
* @param schema Schema
*/
protected void normalizeNullTypeArray31(Schema schema) {
if (!getRule(NORMALIZE_31SPEC) || schema.getTypes() == null || !schema.getTypes().contains("null")) {
return;
}

schema.setNullable(true);
schema.getTypes().remove("null");
if (schema.getTypes().size() == 1) {
schema.setType(String.valueOf(schema.getTypes().iterator().next()));
}
}

protected Schema normalizeSimpleSchema(Schema schema, Set<Schema> visitedSchemas) {
Schema result = processNormalize31Spec(schema, visitedSchemas);
return processSetPrimitiveTypesToNullable(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1983,6 +1983,43 @@ public void testOpenAPINormalizer31SpecNullMapAdditionalProperties() {
assertEquals(errorsValue.getType(), "array");
assertTrue(errorsValue.getNullable());
assertNotNull(errorsValue.getItems());

// the maps themselves are declared `type: [object, "null"]`, so they must end up as
// nullable object schemas rather than keeping the OAS 3.1 type array.
for (String mapProperty : List.of("stringMap", "errorsByKey")) {
Schema map = (Schema) schema.getProperties().get(mapProperty);
assertEquals(map.getType(), "object", mapProperty + " should be normalized to type object");
assertTrue(map.getNullable(), mapProperty + " should be nullable");
}
}

/**
* A map declared with an OAS 3.1 type array (`type: ["null", object]`) whose value schema is a
* `$ref` must be normalized to a nullable object schema, keeping the `$ref` value schema intact.
* Without it the `null` entry stays in the type array and generators resolve the property to a
* fictional `Null` model, dropping both the nullability and the import of the referenced model.
* Regression test for <a href="https://github.com/OpenAPITools/openapi-generator/issues/24517">#24517</a>.
*/
@Test
public void testIssue24517NullableMapWithRefValueUsingTypeArray() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/issue_24517.yaml");
Map<String, String> inputRules = Map.of("NORMALIZE_31SPEC", "true");
new OpenAPINormalizer(openAPI, inputRules).normalize();

Schema map = (Schema) openAPI.getComponents().getSchemas().get("ParentNullableMap")
.getProperties().get("map");
assertEquals(map.getType(), "object");
assertFalse(map.getTypes().contains("null"), "the `null` entry must be removed from the type array");
assertTrue(map.getNullable());
assertEquals(ModelUtils.getAdditionalProperties(map).get$ref(), "#/components/schemas/Child",
"the $ref value schema of the map must be preserved");

// control: the equivalent nullable array was already normalized correctly
Schema list = (Schema) openAPI.getComponents().getSchemas().get("ParentNullableArray")
.getProperties().get("list");
assertEquals(list.getType(), "array");
assertTrue(list.getNullable());
assertEquals(list.getItems().get$ref(), "#/components/schemas/Child");
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,63 @@ public void generatesTrailingCommasInAsConstEnumObjects() throws Exception {
" Xyz: '(xyz)',\n" +
"} as const;");
}

@Test(description = "A nullable map (OAS 3.1 type array) with a $ref value imports the referenced model")
public void testNullableMapWithRefValueImportsReferencedModel() throws Exception {
final File output = Files.createTempDirectory("typescript_axios_nullable_map_ref_").toFile();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("typescript-axios")
.setInputSpec("src/test/resources/3_1/issue_24517.yaml")
.setModelPackage("models")
.setApiPackage("api")
.addAdditionalProperty(TypeScriptAxiosClientCodegen.SEPARATE_MODELS_AND_API, true)
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

final List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
files.forEach(File::deleteOnExit);

// `map` is `type: ["null", object]` with `additionalProperties: {$ref: Child}`: the value model
// has to be imported and the property has to stay nullable. Before the fix the generator
// emitted an import of a `Null` model that it never writes and left `Child` unimported,
// so the output did not compile.
Path nullableMap = Paths.get(output + "/models/parent-nullable-map.ts");
TestUtils.assertFileContains(nullableMap,
"import type { Child } from './child';",
"'map'?: { [key: string]: Child; } | null;");
TestUtils.assertFileNotContains(nullableMap, "from './null'");

// control: the equivalent nullable array of the same $ref was already generated correctly
Path nullableArray = Paths.get(output + "/models/parent-nullable-array.ts");
TestUtils.assertFileContains(nullableArray,
"import type { Child } from './child';",
"'list'?: Array<Child> | null;");
}

@Test(description = "A nullable map (OAS 3.1 type array) with a primitive value keeps its nullability")
public void testNullableMapWithPrimitiveValueKeepsNullability() throws Exception {
final File output = Files.createTempDirectory("typescript_axios_nullable_map_primitive_").toFile();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("typescript-axios")
.setInputSpec("src/test/resources/3_1/issue_24517.yaml")
.setModelPackage("models")
.setApiPackage("api")
.addAdditionalProperty(TypeScriptAxiosClientCodegen.SEPARATE_MODELS_AND_API, true)
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

final List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
files.forEach(File::deleteOnExit);

// Same `type: ["null", object]` map shape, but with a primitive value schema. This form never
// failed to compile - the dangling `Null` import is suppressed by the generated `@ts-ignore`
// and the type declaration was already a map - so the only visible symptom was the missing
// `| null`. It needs its own guard: a future regression could keep imports working while
// dropping nullability again, and that would leave the assertions above green.
Path primitiveMap = Paths.get(output + "/models/parent-nullable-primitive-map.ts");
TestUtils.assertFileContains(primitiveMap, "'counts'?: { [key: string]: number; } | null;");
TestUtils.assertFileNotContains(primitiveMap, "from './null'");
}
}
36 changes: 36 additions & 0 deletions modules/openapi-generator/src/test/resources/3_1/issue_24517.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
openapi: 3.1.0
info:
title: Repro
version: 1.0.0
components:
schemas:
Child:
type: object
properties:
name:
type: string
ParentNullableMap:
type: object
properties:
# a nullable map whose value schema is a $ref
map:
type: ["null", object]
additionalProperties:
$ref: "#/components/schemas/Child"
ParentNullablePrimitiveMap:
type: object
properties:
# the same nullable map shape with a primitive value schema. This one always compiled,
# it just dropped its nullability silently, so it needs its own regression guard.
counts:
type: ["null", object]
additionalProperties:
type: integer
ParentNullableArray:
type: object
properties:
# control: a nullable array of the same $ref, which is already normalized correctly
list:
type: ["null", array]
items:
$ref: "#/components/schemas/Child"
Loading