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 @@ -2941,7 +2941,7 @@ private void mergeProperties(Map<String, Schema> existingProperties, Map<String,
Schema existingType = existingProperties.get("type");
Schema newType = newProperties.get("type");
newProperties.forEach((key, value) ->
existingProperties.put(key, ModelUtils.cloneSchema(value, specVersionGreaterThanOrEqualTo310(openAPI)))
putProperty(existingProperties, key, ModelUtils.cloneSchema(value, specVersionGreaterThanOrEqualTo310(openAPI)))
);
if (null != existingType && null != newType && null != newType.getEnum() && !newType.getEnum().isEmpty()) {
for (Object e : newType.getEnum()) {
Expand Down Expand Up @@ -3608,7 +3608,7 @@ protected void addProperties(Map<String, Schema> properties, List<String> requir
if (ModelUtils.isComposedSchema(schema)) {
// fix issue #16797 and #15796, constructor fail by missing parent required params
if (ModelUtils.hasProperties(schema)) {
properties.putAll(schema.getProperties());
putProperties(properties, schema.getProperties());
}

if (schema.getAllOf() != null) {
Expand Down Expand Up @@ -3642,13 +3642,39 @@ protected void addProperties(Map<String, Schema> properties, List<String> requir
return;
}
if (schema.getProperties() != null) {
properties.putAll(schema.getProperties());
putProperties(properties, schema.getProperties());
}
if (schema.getRequired() != null) {
required.addAll(schema.getRequired());
}
}

/**
* Adds each property to the target map. When a property of the same name is already present
* with type information and the incoming schema carries no type of its own (for example an
* allOf part that only sets 'nullable: true' on an inherited property), the incoming
* constraints (nullable, description, validation keywords, format, default, extensions)
* are applied on top of the existing schema instead of replacing it, so the type is not
* lost. See issue #4128.
*/
private void putProperties(Map<String, Schema> targetProperties, Map<String, Schema> newProperties) {
newProperties.forEach((name, incoming) -> putProperty(targetProperties, name, incoming));
}

private void putProperty(Map<String, Schema> targetProperties, String name, Schema incoming) {
Schema existing = targetProperties.get(name);
if (existing != null && incoming != null
&& !ModelUtils.isAnyType(existing)
&& ModelUtils.isMetadataOnlySchema(incoming)
&& incoming.getEnum() == null && incoming.getConst() == null && incoming.getNot() == null) {
Schema merged = ModelUtils.cloneSchema(existing, specVersionGreaterThanOrEqualTo310(openAPI));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new merge only preserves a hand-picked subset of constraints. isConstraintOnlySchema() matches any typeless schema, so a part that only overrides e.g. maxLength, pattern, format, minimum/maximum or default on an inherited property enters the merge branch, but putProperty copies only nullable/description/deprecated/readOnly/writeOnly/extensions onto the clone — every other keyword is silently dropped while the type is kept. The generated model/validation will therefore be weaker than the OpenAPI spec declares (the constraint is lost instead of degraded). Consider either copying the remaining validation/format fields onto merged, or narrowing isConstraintOnlySchema to only the keywords that putProperty actually merges, so the Javadoc's 'constraints are applied' claim holds and no constraint silently disappears.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java, line 3667:

<comment>The new merge only preserves a hand-picked subset of constraints. isConstraintOnlySchema() matches any typeless schema, so a part that only overrides e.g. `maxLength`, `pattern`, `format`, `minimum`/`maximum` or `default` on an inherited property enters the merge branch, but putProperty copies only nullable/description/deprecated/readOnly/writeOnly/extensions onto the clone — every other keyword is silently dropped while the type is kept. The generated model/validation will therefore be weaker than the OpenAPI spec declares (the constraint is lost instead of degraded). Consider either copying the remaining validation/format fields onto merged, or narrowing isConstraintOnlySchema to only the keywords that putProperty actually merges, so the Javadoc's 'constraints are applied' claim holds and no constraint silently disappears.</comment>

<file context>
@@ -3642,13 +3642,67 @@ protected void addProperties(Map<String, Schema> properties, List<String> requir
+        Schema existing = targetProperties.get(name);
+        if (existing != null && incoming != null
+                && !ModelUtils.isAnyType(existing) && isConstraintOnlySchema(incoming)) {
+            Schema merged = ModelUtils.cloneSchema(existing, specVersionGreaterThanOrEqualTo310(openAPI));
+            if (incoming.getNullable() != null) {
+                merged.setNullable(incoming.getNullable());
</file context>

Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
ModelUtils.copyConstraints(incoming, merged);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
targetProperties.put(name, merged);
} else {
targetProperties.put(name, incoming);
}
}

/**
* Camelize the method name of the getter and setter
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2807,6 +2807,55 @@ public static void copyMetadata(Schema from, Schema to) {
}
}

/**
* Copies metadata plus the validation and format keywords that copyMetadata does not cover.
* Used when an allOf part only constrains an inherited property (see issue #4128), so no
* declared keyword is lost while the type is kept.
*
* @param from schema to copy from
* @param to schema to copy to
*/
public static void copyConstraints(Schema from, Schema to) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: An allOf property overlay containing not is silently lost during the new merge. Because isMetadataOnlySchema does not classify not as a type-defining keyword, putProperty takes the merge branch, but copyConstraints never transfers incoming.getNot(), so the resulting property no longer enforces that constraint. Including not in the copied constraints (or excluding it from the metadata-only merge) would preserve the overlay semantics.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java, line 2773:

<comment>An allOf property overlay containing `not` is silently lost during the new merge. Because `isMetadataOnlySchema` does not classify `not` as a type-defining keyword, `putProperty` takes the merge branch, but `copyConstraints` never transfers `incoming.getNot()`, so the resulting property no longer enforces that constraint. Including `not` in the copied constraints (or excluding it from the metadata-only merge) would preserve the overlay semantics.</comment>

<file context>
@@ -2762,6 +2762,48 @@ public static void copyMetadata(Schema from, Schema to) {
+     * @param from schema to copy from
+     * @param to   schema to copy to
+     */
+    public static void copyConstraints(Schema from, Schema to) {
+        copyMetadata(from, to);
+        if (from.getFormat() != null) {
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix in da0796a, not is excluded from the merge, old replace behavior applies.

Map<String, Object> targetExtensions = to.getExtensions() == null ? null : new HashMap<>(to.getExtensions());
copyMetadata(from, to);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Generated validation can be weakened when the incoming allOf constraint is looser than the inherited one because this merge overwrites existing bounds instead of intersecting them. Combining numeric, length, item, and property bounds using the most restrictive value would preserve the schema’s allOf semantics.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java, line 2774:

<comment>Generated validation can be weakened when the incoming allOf constraint is looser than the inherited one because this merge overwrites existing bounds instead of intersecting them. Combining numeric, length, item, and property bounds using the most restrictive value would preserve the schema’s allOf semantics.</comment>

<file context>
@@ -2762,6 +2762,48 @@ public static void copyMetadata(Schema from, Schema to) {
+     * @param to   schema to copy to
+     */
+    public static void copyConstraints(Schema from, Schema to) {
+        copyMetadata(from, to);
+        if (from.getFormat() != null) {
+            to.setFormat(from.getFormat());
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one is out of scope here. Intersecting bounds would need constraint resolution logic, and overlay-wins is the existing convention in this area (mergeProperties, the normalizer). Before this PR the inherited bounds were dropped entirely, so this is not a regression. Happy to open a follow-up issue for intersection semantics if maintainers want it.

// merge extensions per key instead of replacing, the source wins on conflicts
if (targetExtensions != null && from.getExtensions() != null) {
Map<String, Object> mergedExtensions = new HashMap<>(targetExtensions);
mergedExtensions.putAll(from.getExtensions());
to.setExtensions(mergedExtensions);
}
if (from.getFormat() != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A typeless allOf overlay using const loses its value constraint during merging because this copy routine never transfers const. Copying const (or classifying it as a replacement keyword) would keep generated schemas faithful to the input.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java, line 2775:

<comment>A typeless allOf overlay using `const` loses its value constraint during merging because this copy routine never transfers `const`. Copying `const` (or classifying it as a replacement keyword) would keep generated schemas faithful to the input.</comment>

<file context>
@@ -2762,6 +2762,48 @@ public static void copyMetadata(Schema from, Schema to) {
+     */
+    public static void copyConstraints(Schema from, Schema to) {
+        copyMetadata(from, to);
+        if (from.getFormat() != null) {
+            to.setFormat(from.getFormat());
+        }
</file context>
Suggested change
if (from.getFormat() != null) {
if (from.getConst() != null) {
to.setConst(from.getConst());
}
if (from.getFormat() != null) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in da0796a. Overlays carrying const keep the old replace behavior, const reads as a value redefinition rather than a constraint.

to.setFormat(from.getFormat());
}
if (from.getPattern() != null) {
to.setPattern(from.getPattern());
}
if (from.getExclusiveMaximum() != null) {
to.setExclusiveMaximum(from.getExclusiveMaximum());
}
if (from.getExclusiveMinimum() != null) {
to.setExclusiveMinimum(from.getExclusiveMinimum());
}
if (from.getExclusiveMaximumValue() != null) {
to.setExclusiveMaximumValue(from.getExclusiveMaximumValue());
}
if (from.getExclusiveMinimumValue() != null) {
to.setExclusiveMinimumValue(from.getExclusiveMinimumValue());
}
if (from.getMultipleOf() != null) {
to.setMultipleOf(from.getMultipleOf());
}
if (from.getUniqueItems() != null) {
to.setUniqueItems(from.getUniqueItems());
}
if (from.getMaxProperties() != null) {
to.setMaxProperties(from.getMaxProperties());
}
if (from.getMinProperties() != null) {
to.setMinProperties(from.getMinProperties());
}
}

/**
* Returns true if a schema is only metadata and not an actual type.
* For example, a schema that only has a `description` without any `properties` or `$ref` defined.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,27 @@ public void testAllOfRequired() {
assertEquals(getRequiredVars(childModel), Collections.singletonList("name"));
}

@Test
public void testAllOfNullableWithoutTypeKeepsType() {
// issue #4128: an allOf part that only sets 'nullable: true' on a property
// defined in another part must not erase the property type
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf-nullable-typeless-override.yaml");
DefaultCodegen codegen = new DefaultCodegen();
codegen.setOpenAPI(openAPI);

Schema schema = openAPI.getComponents().getSchemas().get("UpdateFirm");
CodegenModel model = codegen.fromModel("UpdateFirm", schema);

CodegenProperty addressId = model.vars.stream()
.filter(v -> "addressId".equals(v.baseName)).findFirst().orElseThrow();
assertEquals("String", addressId.dataType);
assertTrue(addressId.isNullable);
assertEquals(Integer.valueOf(36), addressId.maxLength);
// extensions from both parts survive the merge
assertEquals("keep", addressId.vendorExtensions.get("x-base-marker"));
assertEquals("added", addressId.vendorExtensions.get("x-overlay-marker"));
}

@Test
public void testAllOfSingleAndDoubleRefWithOwnPropsNoDiscriminator() {
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf_composition.yaml");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
openapi: 3.0.3
info:
title: allOf nullable without type
version: 1.0.0
paths:
/firm/{firmId}:
patch:
operationId: updateFirm
parameters:
- in: path
name: firmId
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateFirm"
responses:
"204":
description: Updated
components:
schemas:
FirmProperties:
properties:
addressId:
type: string
x-base-marker: keep
UpdateFirm:
allOf:
- $ref: "#/components/schemas/FirmProperties"
- properties:
firmName:
type: string
nullable: true
addressId:
nullable: true
maxLength: 36
x-overlay-marker: added