Skip to content
Closed
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 @@ -2814,7 +2814,9 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
Map<String, Schema> newProperties = new LinkedHashMap<>();
addProperties(newProperties, required, refSchema, new HashSet<>());
mergeProperties(properties, newProperties);
addProperties(allProperties, allRequired, refSchema, new HashSet<>());
Map<String, Schema> newAllProperties = new LinkedHashMap<>();
addProperties(newAllProperties, allRequired, refSchema, new HashSet<>());
mergeProperties(allProperties, newAllProperties);
}
}

Expand Down Expand Up @@ -2896,15 +2898,29 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
/**
* Combines all previously-detected type entries for a schema with newly-discovered ones, to ensure
* that schema for items like enum include all possible values.
*
* Properties that appear in multiple sub-schemas with incompatible types are dropped so that a
* parent schema (e.g. a discriminator base type) does not inherit a field whose type differs
* across its oneOf variants.
*/
private void mergeProperties(Map<String, Schema> existingProperties, Map<String, Schema> newProperties) {
// https://github.com/OpenAPITools/openapi-generator/issues/12545
if (null != existingProperties && null != newProperties) {
Schema existingType = existingProperties.get("type");
Schema newType = newProperties.get("type");
newProperties.forEach((key, value) ->
existingProperties.put(key, ModelUtils.cloneSchema(value, specVersionGreaterThanOrEqualTo310(openAPI)))
);
newProperties.forEach((key, value) -> {
if (!existingProperties.containsKey(key)) {
// Property not seen before: pull it up from this sub-schema.
existingProperties.put(key, ModelUtils.cloneSchema(value, specVersionGreaterThanOrEqualTo310(openAPI)));
} else if (!isSchemasTypeCompatible(existingProperties.get(key), value)) {
// Same property name but incompatible types across sub-schemas: drop it so the
// parent does not end up with an ambiguous field (e.g. coordinates: number[]
// in Polygon vs coordinates: number[][] in MultiPolygon).
existingProperties.remove(key);
}
// Compatible type already present: keep the existing entry unchanged.
});
// Merge enum values for the 'type' discriminator property if it is still present.
Schema existingType = existingProperties.get("type");
if (null != existingType && null != newType && null != newType.getEnum() && !newType.getEnum().isEmpty()) {
for (Object e : newType.getEnum()) {
// ensure all interface enum types are added to schema
Expand All @@ -2917,6 +2933,25 @@ private void mergeProperties(Map<String, Schema> existingProperties, Map<String,
}
}

/**
* Returns true when two schemas share the same base type and, for array types, the same
* item-type structure. Used by {@link #mergeProperties} to decide whether a property that
* appears in more than one oneOf/anyOf sub-schema can safely be pulled up into the parent.
*/
private boolean isSchemasTypeCompatible(Schema schema1, Schema schema2) {
if (schema1 == null && schema2 == null) return true;
if (schema1 == null || schema2 == null) return false;
String type1 = ModelUtils.getType(schema1);
String type2 = ModelUtils.getType(schema2);
if (!Objects.equals(type1, type2)) {
return false;
}
if ("array".equals(type1)) {
return isSchemasTypeCompatible(schema1.getItems(), schema2.getItems());
}
return true;
}

protected void updateModelForObject(CodegenModel m, Schema schema) {
if (schema.getProperties() != null || schema.getRequired() != null && !(ModelUtils.isComposedSchema(schema))) {
// passing null to allProperties and allRequired as there's no parent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5107,6 +5107,25 @@ public void testDefaultOauthIsNotNull() {
assertTrue(openIdScheme.isOpenId);
}

@Test
public void testGeoJsonObjectDoesNotContainCoordinates() {
// GeoJsonObject with a discriminator and oneOf (Polygon, MultiPolygon)
// should not have the 'coordinates' field, even though both subtypes define it (but with incompatible types).
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/geojson_discriminator.yaml");
final DefaultCodegen codegen = new DefaultCodegen();

Schema schema = openAPI.getComponents().getSchemas().get("GeoJsonObject");
codegen.setOpenAPI(openAPI);
CodegenModel geoJsonObject = codegen.fromModel("GeoJsonObject", schema);

boolean coordinatesInVars = geoJsonObject.vars.stream()
.anyMatch(cp -> "coordinates".equals(cp.baseName));
boolean coordinatesInAllVars = geoJsonObject.allVars.stream()
.anyMatch(cp -> "coordinates".equals(cp.baseName));
assertFalse(coordinatesInVars);
assertFalse(coordinatesInAllVars);
}

private List<String> getRequiredVars(CodegenModel model) {
return getNames(model.getRequiredVars());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4325,4 +4325,32 @@ public void testJspecify(String library, boolean useSpringBoot4, boolean hasJspe
.fileContains("@org.jspecify.annotations.NullMarked");

}

@Test
public void testGeoJsonObjectDoesNotContainCoordinates() {
// The generated GeoJsonObject class should not have a 'coordinates' field,
// even though both Polygon and MultiPolygon (its oneOf subtypes) define one.
// Polygon has coordinates: List<Double>, MultiPolygon has coordinates: List<List<Double>>,
// so the types are incompatible and the field must not be pulled up to the parent.
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/geojson_discriminator.yaml");
final JavaClientCodegen codegen = new JavaClientCodegen();
codegen.setOpenAPI(openAPI);
codegen.setOutputDir(newTempFolder().toString());

Map<String, File> files = new DefaultGenerator()
.opts(new ClientOptInput().openAPI(openAPI).config(codegen))
.generate()
.stream()
.collect(Collectors.toMap(File::getName, Function.identity()));

// GeoJsonObject must not expose coordinates — the types differ across oneOf variants
JavaFileAssert.assertThat(files.get("GeoJsonObject.java"))
.fileDoesNotContain("coordinates");

// Each subtype must still carry its own coordinates field
JavaFileAssert.assertThat(files.get("Polygon.java"))
.assertProperty("coordinates");
JavaFileAssert.assertThat(files.get("MultiPolygon.java"))
.assertProperty("coordinates");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
openapi: 3.0.3
info:
title: GeoJSON Discriminator Test
version: 1.0.0
paths: {}
components:
schemas:
GeoJsonObject:
type: object
properties:
type:
type: string
required:
- type
discriminator:
propertyName: type
oneOf:
- $ref: '#/components/schemas/Polygon'
- $ref: '#/components/schemas/MultiPolygon'
Polygon:
allOf:
- $ref: '#/components/schemas/GeoJsonObject'
- type: object
properties:
coordinates:
type: array
items:
type: number
format: double
MultiPolygon:
allOf:
- $ref: '#/components/schemas/GeoJsonObject'
- type: object
properties:
coordinates:
type: array
items:
type: array
items:
type: number
format: double