From f8ddd3b01baf1b4f956fb94eda50dd39905006d5 Mon Sep 17 00:00:00 2001 From: Thorsten Hirsch Date: Wed, 18 Feb 2026 09:14:00 +0100 Subject: [PATCH 1/6] [Java][native] Fix oneOf wrapper inheritance and inherited readOnly @JsonCreator --- .../openapitools/codegen/DefaultCodegen.java | 17 ++++- .../codegen/utils/ModelUtils.java | 24 ++++++ .../Java/libraries/native/pojo.mustache | 15 +++- .../codegen/DefaultCodegenTest.java | 76 +++++++++++++++++++ .../issue-inherited-readonly-jsoncreator.yaml | 60 +++++++++++++++ .../3_0/issue-oneOf-wrapper-inheritance.yaml | 58 ++++++++++++++ 6 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/issue-inherited-readonly-jsoncreator.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/issue-oneOf-wrapper-inheritance.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index d83cdef420cf..29b9e8950e92 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -2794,7 +2794,13 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map newProperties = new LinkedHashMap<>(); + addProperties(newProperties, required, refSchema, new HashSet<>()); + mergeProperties(properties, newProperties); + addProperties(allProperties, allRequired, refSchema, new HashSet<>()); + } else if (allParents.contains(ref) && supportsMultipleInheritance) { // multiple inheritance addProperties(allProperties, allRequired, refSchema, new HashSet<>()); } else if (parentName != null && parentName.equals(ref) && supportsInheritance) { @@ -3156,6 +3162,15 @@ public CodegenModel fromModel(String name, Schema schema) { // remove duplicated properties m.removeAllDuplicatedProperty(); + // Mark inherited readonly properties for template to use setter instead of direct field access + if (m.parent != null && m.readOnlyVars != null) { + for (CodegenProperty roVar : m.readOnlyVars) { + if (!Boolean.TRUE.equals(roVar.isOverridden)) { + roVar.vendorExtensions.put("x-is-inherited-readonly", Boolean.TRUE); + } + } + } + // set isDiscriminator on the discriminator property if (m.discriminator != null) { String discPropName = m.discriminator.getPropertyBaseName(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index a8f62b1b8331..a59d13e1ee16 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -1584,6 +1584,10 @@ public static String getParentName(Schema composedSchema, Map al if (s == null) { LOGGER.error("Failed to obtain schema from {}", parentName); parentNameCandidates.add("UNKNOWN_PARENT_NAME"); + } else if (isOneOfWrapperSchema(s)) { + // Skip oneOf wrapper schemas (generate AbstractOpenApiSchema subclasses) + hasAmbiguousParents = true; + refedWithoutDiscriminator.add(parentName); } else if (hasOrInheritsDiscriminator(s, allSchemas, new ArrayList())) { // discriminator.propertyName is used or x-parent is used parentNameCandidates.add(parentName); @@ -1650,6 +1654,8 @@ private static List getAllParentsName( if (s == null) { LOGGER.error("Failed to obtain schema from {}", parentName); names.add("UNKNOWN_PARENT_NAME"); + } else if (isOneOfWrapperSchema(s)) { + // Skip oneOf wrapper schemas - properties will be inlined } else if (hasOrInheritsDiscriminator(s, allSchemas, new ArrayList())) { // discriminator.propertyName is used or x-parent is used names.add(parentName); @@ -2122,6 +2128,24 @@ public static boolean hasOneOf(Schema schema) { return false; } + /** + * Returns true if the schema is a oneOf wrapper (has oneOf + properties/discriminator). + * Such schemas generate AbstractOpenApiSchema subclasses and cannot be used as parents. + * + * @param schema the schema + * @return true if the schema is a oneOf wrapper + */ + public static boolean isOneOfWrapperSchema(Schema schema) { + if (schema == null) { + return false; + } + boolean hasOneOf = schema.getOneOf() != null && !schema.getOneOf().isEmpty(); + boolean hasDiscriminator = schema.getDiscriminator() != null && + schema.getDiscriminator().getPropertyName() != null; + boolean hasProperties = schema.getProperties() != null && !schema.getProperties().isEmpty(); + return hasOneOf && (hasDiscriminator || hasProperties); + } + /** * Returns true if the schema contains anyOf but * no properties/allOf/anyOf defined. diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache index abdf16d12009..2e5fa122f0a9 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache @@ -95,7 +95,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens ) { this(); {{#readOnlyVars}} - this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; + {{#vendorExtensions.x-is-inherited-readonly}}this.set{{nameInPascalCase}}({{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}});{{/vendorExtensions.x-is-inherited-readonly}}{{^vendorExtensions.x-is-inherited-readonly}}this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};{{/vendorExtensions.x-is-inherited-readonly}} {{/readOnlyVars}} }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} {{#vars}} @@ -254,6 +254,19 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{/isReadOnly}} + {{#isReadOnly}} + /** + * Protected setter for {{name}} (readOnly property, used by subclasses' @JsonCreator). + */ + protected void {{setter}}({{>nullable_var_annotations}} {{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + {{/isReadOnly}} {{/vars}} {{>libraries/native/additional_properties}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 32eee9f1969a..4cf06f717655 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -5074,4 +5074,80 @@ private List getNames(List props) { if (props == null) return null; return props.stream().map(v -> v.name).collect(Collectors.toList()); } + + /** + * Test that oneOf wrapper schemas are not used as parent classes. + * Schemas with oneOf + properties generate AbstractOpenApiSchema subclasses + * which cannot be extended. Properties should be inlined instead. + */ + @Test + public void testOneOfWrapperSchemaShouldNotBeUsedAsParent() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/issue-oneOf-wrapper-inheritance.yaml"); + codegen.setOpenAPI(openAPI); + + Schema clickEventSchema = openAPI.getComponents().getSchemas().get("ClickEvent"); + CodegenModel clickEventModel = codegen.fromModel("ClickEvent", clickEventSchema); + + assertNull(clickEventModel.parent, "ClickEvent should not have EventWrapper as parent"); + + Set clickEventVarNames = clickEventModel.vars.stream() + .map(v -> v.name) + .collect(Collectors.toSet()); + assertTrue(clickEventVarNames.contains("timestamp"), "ClickEvent should have 'timestamp' inlined"); + assertTrue(clickEventVarNames.contains("userId"), "ClickEvent should have 'userId' inlined"); + assertTrue(clickEventVarNames.contains("sessionId"), "ClickEvent should have 'sessionId' inlined"); + assertTrue(clickEventVarNames.contains("elementId"), "ClickEvent should have 'elementId'"); + assertTrue(clickEventVarNames.contains("clickX"), "ClickEvent should have 'clickX'"); + assertTrue(clickEventVarNames.contains("clickY"), "ClickEvent should have 'clickY'"); + } + + @Test + public void testIsOneOfWrapperSchema() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/issue-oneOf-wrapper-inheritance.yaml"); + + Schema eventWrapperSchema = openAPI.getComponents().getSchemas().get("EventWrapper"); + assertTrue(ModelUtils.isOneOfWrapperSchema(eventWrapperSchema), + "EventWrapper should be detected as oneOf wrapper"); + + Schema clickEventSchema = openAPI.getComponents().getSchemas().get("ClickEvent"); + assertFalse(ModelUtils.isOneOfWrapperSchema(clickEventSchema), + "ClickEvent should not be detected as oneOf wrapper"); + + Schema scrollEventSchema = openAPI.getComponents().getSchemas().get("ScrollEvent"); + assertFalse(ModelUtils.isOneOfWrapperSchema(scrollEventSchema), + "ScrollEvent should not be detected as oneOf wrapper"); + } + + /** + * Test that inherited readOnly properties use protected setters in @JsonCreator. + * Child classes must use setters for inherited readOnly properties since they're private in parent. + */ + @Test + public void testInheritedReadOnlyPropertiesHaveProtectedSetters() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/issue-inherited-readonly-jsoncreator.yaml"); + codegen.setOpenAPI(openAPI); + + Schema baseModelSchema = openAPI.getComponents().getSchemas().get("BaseModel"); + CodegenModel baseModel = codegen.fromModel("BaseModel", baseModelSchema); + + assertTrue(baseModel.readOnlyVars != null && !baseModel.readOnlyVars.isEmpty(), + "BaseModel should have readOnly properties"); + + Schema extendedModelSchema = openAPI.getComponents().getSchemas().get("ExtendedModel"); + CodegenModel extendedModel = codegen.fromModel("ExtendedModel", extendedModelSchema); + + assertEquals("BaseModel", extendedModel.parent, "ExtendedModel should extend BaseModel"); + + boolean foundInheritedReadOnly = false; + for (CodegenProperty roVar : extendedModel.readOnlyVars) { + if (!Boolean.TRUE.equals(roVar.isOverridden)) { + foundInheritedReadOnly = true; + assertTrue(Boolean.TRUE.equals(roVar.vendorExtensions.get("x-is-inherited-readonly")), + "Inherited readOnly '" + roVar.name + "' should have x-is-inherited-readonly"); + } + } + assertTrue(foundInheritedReadOnly, "ExtendedModel should have inherited readOnly properties"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue-inherited-readonly-jsoncreator.yaml b/modules/openapi-generator/src/test/resources/3_0/issue-inherited-readonly-jsoncreator.yaml new file mode 100644 index 000000000000..56723c428d95 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue-inherited-readonly-jsoncreator.yaml @@ -0,0 +1,60 @@ +openapi: 3.0.3 +info: + title: Test API for inherited readonly properties @JsonCreator fix + version: 1.0.0 + description: | + This spec demonstrates the readOnly fix works when there's proper inheritance. + + IMPORTANT NOTE about discriminator: + - The discriminator IS required by OpenAPI Generator to establish the parent-child relationship + - Without a discriminator (or x-parent extension), allOf refs are not recognized as inheritance + - The readOnly fix itself works on any inherited readOnly properties, but inheritance + must first be established via discriminator or x-parent + + The readOnly fix ensures that when a child class inherits readOnly properties from a parent, + the child's @JsonCreator constructor uses protected setters (not direct field access) + for the inherited properties, since they are private in the parent. +paths: {} +components: + schemas: + # Parent with readOnly properties - discriminator REQUIRED for inheritance recognition + # The discriminator is NOT specifically for the readOnly fix, but for inheritance itself + BaseModel: + type: object + discriminator: + propertyName: modelType + properties: + id: + type: string + format: uuid + readOnly: true + description: Read-only identifier inherited from parent + createdAt: + type: string + format: date-time + readOnly: true + description: Read-only creation timestamp inherited from parent + modelType: + type: string + description: Discriminator property for inheritance + name: + type: string + description: Regular read-write property + required: + - id + - modelType + - name + + # Child extends parent - @JsonCreator should use protected setters for inherited readOnly properties + ExtendedModel: + allOf: + - $ref: '#/components/schemas/BaseModel' + - type: object + properties: + description: + type: string + status: + type: string + enum: [active, inactive] + required: + - status diff --git a/modules/openapi-generator/src/test/resources/3_0/issue-oneOf-wrapper-inheritance.yaml b/modules/openapi-generator/src/test/resources/3_0/issue-oneOf-wrapper-inheritance.yaml new file mode 100644 index 000000000000..025f198c1a9e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue-oneOf-wrapper-inheritance.yaml @@ -0,0 +1,58 @@ +openapi: 3.0.3 +info: + title: Test API for oneOf wrapper schema inheritance fix (WITHOUT discriminator) + version: 1.0.0 + description: | + This spec demonstrates the oneOf wrapper fix works without requiring a discriminator. + A oneOf wrapper schema (oneOf + properties, without discriminator) cannot be used as + a parent class. Instead, its properties should be inlined into child schemas. +paths: {} +components: + schemas: + # This schema has oneOf AND properties but NO discriminator - it's still a "oneOf wrapper" + # It should NOT be used as a parent class for inheritance + EventWrapper: + oneOf: + - $ref: '#/components/schemas/ClickEvent' + - $ref: '#/components/schemas/ScrollEvent' + properties: + timestamp: + type: string + format: date-time + readOnly: true + userId: + type: string + sessionId: + type: string + required: + - timestamp + - userId + + # This schema uses allOf to reference the oneOf wrapper + # Properties from EventWrapper should be inlined, not inherited + ClickEvent: + allOf: + - $ref: '#/components/schemas/EventWrapper' + - type: object + properties: + elementId: + type: string + clickX: + type: integer + clickY: + type: integer + required: + - elementId + + ScrollEvent: + allOf: + - $ref: '#/components/schemas/EventWrapper' + - type: object + properties: + scrollDepth: + type: integer + direction: + type: string + enum: [up, down, left, right] + required: + - scrollDepth From d1e127d00841f4cdaea03d42c995d36314de71bb Mon Sep 17 00:00:00 2001 From: Thorsten Hirsch Date: Thu, 19 Feb 2026 13:56:16 +0100 Subject: [PATCH 2/6] fix mustache template --- .../src/main/resources/Java/libraries/native/pojo.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache index 2e5fa122f0a9..430824711371 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache @@ -272,7 +272,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens {{>libraries/native/additional_properties}} {{#parent}} - {{#allVars}} + {{#readWriteVars}} {{#isOverridden}} @Override public {{classname}} {{name}}({{>nullable_var_annotations}} {{{datatypeWithEnum}}} {{name}}) { @@ -286,7 +286,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens } {{/isOverridden}} - {{/allVars}} + {{/readWriteVars}} {{/parent}} /** * Return true if this {{name}} object is equal to o. From 35e1a2f2e4522e9a3a2f821157772c1fc1cd363f Mon Sep 17 00:00:00 2001 From: Thorsten Hirsch Date: Thu, 19 Feb 2026 18:57:17 +0100 Subject: [PATCH 3/6] update java petstore samples --- .../openapitools/client/model/HasOnlyReadOnly.java | 12 ++++++++++++ .../java/org/openapitools/client/model/Name.java | 12 ++++++++++++ .../org/openapitools/client/model/ReadOnlyFirst.java | 6 ++++++ .../openapitools/client/model/HasOnlyReadOnly.java | 12 ++++++++++++ .../java/org/openapitools/client/model/Name.java | 12 ++++++++++++ .../org/openapitools/client/model/ReadOnlyFirst.java | 6 ++++++ 6 files changed, 60 insertions(+) diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/HasOnlyReadOnly.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/HasOnlyReadOnly.java index bec3a4e9f87e..9c0c5c0a95ad 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/HasOnlyReadOnly.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/HasOnlyReadOnly.java @@ -71,6 +71,12 @@ public String getBar() { } + /** + * Protected setter for bar (readOnly property, used by subclasses' @JsonCreator). + */ + protected void setBar(@javax.annotation.Nullable String bar) { + this.bar = bar; + } /** @@ -85,6 +91,12 @@ public String getFoo() { } + /** + * Protected setter for foo (readOnly property, used by subclasses' @JsonCreator). + */ + protected void setFoo(@javax.annotation.Nullable String foo) { + this.foo = foo; + } /** diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/Name.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/Name.java index debcb4a648b6..88ece29b9e35 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/Name.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/Name.java @@ -105,6 +105,12 @@ public Integer getSnakeCase() { } + /** + * Protected setter for snakeCase (readOnly property, used by subclasses' @JsonCreator). + */ + protected void setSnakeCase(@javax.annotation.Nullable Integer snakeCase) { + this.snakeCase = snakeCase; + } public Name property(@javax.annotation.Nullable String property) { @@ -143,6 +149,12 @@ public Integer get123number() { } + /** + * Protected setter for _123number (readOnly property, used by subclasses' @JsonCreator). + */ + protected void set123number(@javax.annotation.Nullable Integer _123number) { + this._123number = _123number; + } /** diff --git a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/ReadOnlyFirst.java b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/ReadOnlyFirst.java index 3d91568f003b..6217160eea7b 100644 --- a/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/ReadOnlyFirst.java +++ b/samples/client/petstore/java/native-async/src/main/java/org/openapitools/client/model/ReadOnlyFirst.java @@ -69,6 +69,12 @@ public String getBar() { } + /** + * Protected setter for bar (readOnly property, used by subclasses' @JsonCreator). + */ + protected void setBar(@javax.annotation.Nullable String bar) { + this.bar = bar; + } public ReadOnlyFirst baz(@javax.annotation.Nullable String baz) { diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/HasOnlyReadOnly.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/HasOnlyReadOnly.java index 2c86c78e99f3..c1ccfbe55f5c 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/HasOnlyReadOnly.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/HasOnlyReadOnly.java @@ -73,6 +73,12 @@ public String getBar() { } + /** + * Protected setter for bar (readOnly property, used by subclasses' @JsonCreator). + */ + protected void setBar(@javax.annotation.Nullable String bar) { + this.bar = bar; + } /** @@ -87,6 +93,12 @@ public String getFoo() { } + /** + * Protected setter for foo (readOnly property, used by subclasses' @JsonCreator). + */ + protected void setFoo(@javax.annotation.Nullable String foo) { + this.foo = foo; + } /** diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/Name.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/Name.java index 3fdecf6fb353..32c2dbcbc691 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/Name.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/Name.java @@ -107,6 +107,12 @@ public Integer getSnakeCase() { } + /** + * Protected setter for snakeCase (readOnly property, used by subclasses' @JsonCreator). + */ + protected void setSnakeCase(@javax.annotation.Nullable Integer snakeCase) { + this.snakeCase = snakeCase; + } public Name property(@javax.annotation.Nullable String property) { @@ -145,6 +151,12 @@ public Integer get123number() { } + /** + * Protected setter for _123number (readOnly property, used by subclasses' @JsonCreator). + */ + protected void set123number(@javax.annotation.Nullable Integer _123number) { + this._123number = _123number; + } /** diff --git a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/ReadOnlyFirst.java b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/ReadOnlyFirst.java index 7c436cb400cd..46614c8e0fda 100644 --- a/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/ReadOnlyFirst.java +++ b/samples/client/petstore/java/native/src/main/java/org/openapitools/client/model/ReadOnlyFirst.java @@ -71,6 +71,12 @@ public String getBar() { } + /** + * Protected setter for bar (readOnly property, used by subclasses' @JsonCreator). + */ + protected void setBar(@javax.annotation.Nullable String bar) { + this.bar = bar; + } public ReadOnlyFirst baz(@javax.annotation.Nullable String baz) { From 05208f7cf5eab4b452c6aa3d6688aec9d0763151 Mon Sep 17 00:00:00 2001 From: Thorsten Hirsch Date: Sat, 21 Feb 2026 04:28:39 +0100 Subject: [PATCH 4/6] fix type mismatch cubic-dev-ai has found --- .../src/main/resources/Java/libraries/native/pojo.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache index 430824711371..5b48de4c3080 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache @@ -95,7 +95,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens ) { this(); {{#readOnlyVars}} - {{#vendorExtensions.x-is-inherited-readonly}}this.set{{nameInPascalCase}}({{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}});{{/vendorExtensions.x-is-inherited-readonly}}{{^vendorExtensions.x-is-inherited-readonly}}this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};{{/vendorExtensions.x-is-inherited-readonly}} + {{#vendorExtensions.x-is-inherited-readonly}}this.set{{nameInPascalCase}}({{name}});{{/vendorExtensions.x-is-inherited-readonly}}{{^vendorExtensions.x-is-inherited-readonly}}this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};{{/vendorExtensions.x-is-inherited-readonly}} {{/readOnlyVars}} }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} {{#vars}} From 04a29d1823098660280f16c50dc7ea4e7ed0bf0b Mon Sep 17 00:00:00 2001 From: Thorsten Hirsch Date: Sun, 22 Feb 2026 15:54:30 +0100 Subject: [PATCH 5/6] fix inverted logic cubic-dev-ai has found --- .../openapitools/codegen/DefaultCodegen.java | 18 +++++++++++++++--- .../codegen/DefaultCodegenTest.java | 15 ++------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 29b9e8950e92..8c14065216dd 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -2794,7 +2794,6 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map newProperties = new LinkedHashMap<>(); addProperties(newProperties, required, refSchema, new HashSet<>()); @@ -3162,10 +3161,23 @@ public CodegenModel fromModel(String name, Schema schema) { // remove duplicated properties m.removeAllDuplicatedProperty(); - // Mark inherited readonly properties for template to use setter instead of direct field access if (m.parent != null && m.readOnlyVars != null) { + Schema parentSchema = null; + if (allDefinitions != null && m.parentSchema != null) { + parentSchema = allDefinitions.get(m.parentSchema); + } + + Set parentReadOnlyNames = new HashSet<>(); + if (parentSchema != null && parentSchema.getProperties() != null) { + for (Map.Entry entry : ((Map) parentSchema.getProperties()).entrySet()) { + if (Boolean.TRUE.equals(entry.getValue().getReadOnly())) { + parentReadOnlyNames.add(entry.getKey()); + } + } + } + for (CodegenProperty roVar : m.readOnlyVars) { - if (!Boolean.TRUE.equals(roVar.isOverridden)) { + if (Boolean.TRUE.equals(roVar.isOverridden) || parentReadOnlyNames.contains(roVar.baseName)) { roVar.vendorExtensions.put("x-is-inherited-readonly", Boolean.TRUE); } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 4cf06f717655..a1b904d88538 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -5075,11 +5075,6 @@ private List getNames(List props) { return props.stream().map(v -> v.name).collect(Collectors.toList()); } - /** - * Test that oneOf wrapper schemas are not used as parent classes. - * Schemas with oneOf + properties generate AbstractOpenApiSchema subclasses - * which cannot be extended. Properties should be inlined instead. - */ @Test public void testOneOfWrapperSchemaShouldNotBeUsedAsParent() { final DefaultCodegen codegen = new DefaultCodegen(); @@ -5119,10 +5114,6 @@ public void testIsOneOfWrapperSchema() { "ScrollEvent should not be detected as oneOf wrapper"); } - /** - * Test that inherited readOnly properties use protected setters in @JsonCreator. - * Child classes must use setters for inherited readOnly properties since they're private in parent. - */ @Test public void testInheritedReadOnlyPropertiesHaveProtectedSetters() { final DefaultCodegen codegen = new DefaultCodegen(); @@ -5142,12 +5133,10 @@ public void testInheritedReadOnlyPropertiesHaveProtectedSetters() { boolean foundInheritedReadOnly = false; for (CodegenProperty roVar : extendedModel.readOnlyVars) { - if (!Boolean.TRUE.equals(roVar.isOverridden)) { + if (Boolean.TRUE.equals(roVar.vendorExtensions.get("x-is-inherited-readonly"))) { foundInheritedReadOnly = true; - assertTrue(Boolean.TRUE.equals(roVar.vendorExtensions.get("x-is-inherited-readonly")), - "Inherited readOnly '" + roVar.name + "' should have x-is-inherited-readonly"); } } - assertTrue(foundInheritedReadOnly, "ExtendedModel should have inherited readOnly properties"); + assertTrue(foundInheritedReadOnly, "ExtendedModel should have inherited readOnly properties with x-is-inherited-readonly vendor extension"); } } From bd9f46ef3229cede2d1e1ec884305e5636596399 Mon Sep 17 00:00:00 2001 From: Thorsten Hirsch Date: Sun, 22 Feb 2026 16:54:42 +0100 Subject: [PATCH 6/6] fix more issues --- .../org/openapitools/codegen/DefaultCodegen.java | 13 ++++++++----- .../resources/Java/libraries/native/pojo.mustache | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 8c14065216dd..69158d944c90 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3167,12 +3167,15 @@ public CodegenModel fromModel(String name, Schema schema) { parentSchema = allDefinitions.get(m.parentSchema); } + Map parentProperties = new LinkedHashMap<>(); + if (parentSchema != null) { + addProperties(parentProperties, new ArrayList<>(), parentSchema, new HashSet<>()); + } + Set parentReadOnlyNames = new HashSet<>(); - if (parentSchema != null && parentSchema.getProperties() != null) { - for (Map.Entry entry : ((Map) parentSchema.getProperties()).entrySet()) { - if (Boolean.TRUE.equals(entry.getValue().getReadOnly())) { - parentReadOnlyNames.add(entry.getKey()); - } + for (Map.Entry entry : parentProperties.entrySet()) { + if (Boolean.TRUE.equals(entry.getValue().getReadOnly())) { + parentReadOnlyNames.add(entry.getKey()); } } diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache index 5b48de4c3080..56e16c8e6b51 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache @@ -95,7 +95,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens ) { this(); {{#readOnlyVars}} - {{#vendorExtensions.x-is-inherited-readonly}}this.set{{nameInPascalCase}}({{name}});{{/vendorExtensions.x-is-inherited-readonly}}{{^vendorExtensions.x-is-inherited-readonly}}this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};{{/vendorExtensions.x-is-inherited-readonly}} + {{#vendorExtensions.x-is-inherited-readonly}}this.{{setter}}({{name}});{{/vendorExtensions.x-is-inherited-readonly}}{{^vendorExtensions.x-is-inherited-readonly}}this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};{{/vendorExtensions.x-is-inherited-readonly}} {{/readOnlyVars}} }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} {{#vars}}