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 @@ -2794,7 +2794,12 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
addImport(composed, refSchema, m, modelName);

if (allDefinitions != null && refSchema != null) {
if (allParents.contains(ref) && supportsMultipleInheritance) {
if (ModelUtils.isOneOfWrapperSchema(refSchema)) {
Map<String, Schema> 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) {
Expand Down Expand Up @@ -3156,6 +3161,31 @@ public CodegenModel fromModel(String name, Schema schema) {
// remove duplicated properties
m.removeAllDuplicatedProperty();

if (m.parent != null && m.readOnlyVars != null) {
Schema parentSchema = null;
if (allDefinitions != null && m.parentSchema != null) {
parentSchema = allDefinitions.get(m.parentSchema);
}

Map<String, Schema> parentProperties = new LinkedHashMap<>();
if (parentSchema != null) {
addProperties(parentProperties, new ArrayList<>(), parentSchema, new HashSet<>());
}

Set<String> parentReadOnlyNames = new HashSet<>();
for (Map.Entry<String, Schema> entry : parentProperties.entrySet()) {
if (Boolean.TRUE.equals(entry.getValue().getReadOnly())) {
parentReadOnlyNames.add(entry.getKey());
}
}

for (CodegenProperty roVar : m.readOnlyVars) {
if (Boolean.TRUE.equals(roVar.isOverridden) || parentReadOnlyNames.contains(roVar.baseName)) {
roVar.vendorExtensions.put("x-is-inherited-readonly", Boolean.TRUE);
}
}
}

// set isDiscriminator on the discriminator property
if (m.discriminator != null) {
String discPropName = m.discriminator.getPropertyBaseName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,10 @@ public static String getParentName(Schema composedSchema, Map<String, Schema> 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<Schema>())) {
// discriminator.propertyName is used or x-parent is used
parentNameCandidates.add(parentName);
Expand Down Expand Up @@ -1650,6 +1654,8 @@ private static List<String> 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<Schema>())) {
// discriminator.propertyName is used or x-parent is used
names.add(parentName);
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.{{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}}
Expand Down Expand Up @@ -254,12 +254,25 @@ 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}}

{{#parent}}
{{#allVars}}
{{#readWriteVars}}
{{#isOverridden}}
@Override
public {{classname}} {{name}}({{>nullable_var_annotations}} {{{datatypeWithEnum}}} {{name}}) {
Expand All @@ -273,7 +286,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
}

{{/isOverridden}}
{{/allVars}}
{{/readWriteVars}}
{{/parent}}
/**
* Return true if this {{name}} object is equal to o.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5074,4 +5074,69 @@ private List<String> getNames(List<CodegenProperty> props) {
if (props == null) return null;
return props.stream().map(v -> v.name).collect(Collectors.toList());
}

@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<String> 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
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.vendorExtensions.get("x-is-inherited-readonly"))) {
foundInheritedReadOnly = true;
}
}
assertTrue(foundInheritedReadOnly, "ExtendedModel should have inherited readOnly properties with x-is-inherited-readonly vendor extension");
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


/**
Expand All @@ -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;
}


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading