Skip to content

Add option to generate a fully sealed model in the JavaSpring generator #20503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions docs/generators/java-camel.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
|useOptional|Use Optional container for optional parameters| |false|
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|
|useSealed|Whether to generate sealed model interfaces and classes| |false|
|useSpringBoot3|Generate code and provide dependencies for use with Spring Boot 3.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
|useSpringController|Annotate the generated API as a Spring Controller| |false|
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
|useOptional|Use Optional container for optional parameters| |false|
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|
|useSealed|Whether to generate sealed model interfaces and classes| |false|
|useSpringBoot3|Generate code and provide dependencies for use with Spring Boot 3.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
|useSpringController|Annotate the generated API as a Spring Controller| |false|
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|
Expand Down
30 changes: 24 additions & 6 deletions flake.lock
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you intend to include these changes? If yes that's fine. Just want to check, since you didn't include this in your initial PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just ran nix flake update as I wanted to make sure that it has the latest JDK11 build (got burned in the past in a minor jdk11 release). I can revert if that is preferred.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine, leave it as it is. :) No need to revert.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
public List<CodegenModel> interfaceModels;
@Getter @Setter
public List<CodegenModel> children;
@Getter @Setter
public List<CodegenModel> directChildren;

// anyOf, oneOf, allOf
public Set<String> anyOf = new TreeSet<>();
Expand Down Expand Up @@ -170,6 +172,7 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
public boolean hasOptional;
public boolean isArray;
public boolean hasChildren;
public boolean hasDirectChildren;
public boolean isMap;
/** datatype is the generic inner parameter of a std::optional for C++, or Optional (Java) */
public boolean isOptional;
Expand Down Expand Up @@ -888,6 +891,7 @@ public boolean equals(Object o) {
hasOptional == that.hasOptional &&
isArray == that.isArray &&
hasChildren == that.hasChildren &&
hasDirectChildren == that.hasDirectChildren &&
isMap == that.isMap &&
isOptional == that.isOptional &&
isDeprecated == that.isDeprecated &&
Expand Down Expand Up @@ -922,6 +926,7 @@ public boolean equals(Object o) {
Objects.equals(parentModel, that.parentModel) &&
Objects.equals(interfaceModels, that.interfaceModels) &&
Objects.equals(children, that.children) &&
Objects.equals(directChildren, that.directChildren) &&
Objects.equals(anyOf, that.anyOf) &&
Objects.equals(oneOf, that.oneOf) &&
Objects.equals(allOf, that.allOf) &&
Expand Down Expand Up @@ -975,15 +980,15 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
return Objects.hash(getParent(), getParentSchema(), getInterfaces(), getAllParents(), getParentModel(),
getInterfaceModels(), getChildren(), anyOf, oneOf, allOf, getName(), getSchemaName(), getClassname(), getTitle(),
getInterfaceModels(), getChildren(), getDirectChildren(), anyOf, oneOf, allOf, getName(), getSchemaName(), getClassname(), getTitle(),
getDescription(), getClassVarName(), getModelJson(), getDataType(), getXmlPrefix(), getXmlNamespace(),
getXmlName(), getClassFilename(), getUnescapedDescription(), getDiscriminator(), getDefaultValue(),
getArrayModelType(), isAlias, isString, isInteger, isLong, isNumber, isNumeric, isFloat, isDouble,
isDate, isDateTime, isNull, hasValidation, isShort, isUnboundedInteger, isBoolean,
getVars(), getAllVars(), getNonNullableVars(), getRequiredVars(), getOptionalVars(), getReadOnlyVars(), getReadWriteVars(),
getParentVars(), getAllowableValues(), getMandatory(), getAllMandatory(), getImports(), hasVars,
isEmptyVars(), hasMoreModels, hasEnums, isEnum, isNullable, hasRequired, hasOptional, isArray,
hasChildren, isMap, isOptional, isDeprecated, hasReadOnly, hasOnlyReadOnly, getExternalDocumentation(), getVendorExtensions(),
hasChildren, hasDirectChildren, isMap, isOptional, isDeprecated, hasReadOnly, hasOnlyReadOnly, getExternalDocumentation(), getVendorExtensions(),
getAdditionalPropertiesType(), getMaxProperties(), getMinProperties(), getUniqueItems(), getMaxItems(),
getMinItems(), getMaxLength(), getMinLength(), getExclusiveMinimum(), getExclusiveMaximum(), getMinimum(),
getMaximum(), getPattern(), getMultipleOf(), getItems(), getAdditionalProperties(), getIsModel(),
Expand All @@ -1005,6 +1010,7 @@ public String toString() {
sb.append(", allParents=").append(allParents);
sb.append(", parentModel=").append(parentModel);
sb.append(", children=").append(children != null ? children.size() : "[]");
sb.append(", directChildren=").append(directChildren != null ? directChildren.size() : "[]");
sb.append(", anyOf=").append(anyOf);
sb.append(", oneOf=").append(oneOf);
sb.append(", allOf=").append(allOf);
Expand Down Expand Up @@ -1057,6 +1063,7 @@ public String toString() {
sb.append(", hasOptional=").append(hasOptional);
sb.append(", isArray=").append(isArray);
sb.append(", hasChildren=").append(hasChildren);
sb.append(", hasDirectChildren=").append(hasDirectChildren);
sb.append(", isMap=").append(isMap);
sb.append(", isOptional=").append(isOptional);
sb.append(", isDeprecated=").append(isDeprecated);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,16 +621,29 @@ public Map<String, ModelsMap> updateAllModels(Map<String, ModelsMap> objs) {

// Let parent know about all its children
for (Map.Entry<String, CodegenModel> allModelsEntry : allModels.entrySet()) {
String name = allModelsEntry.getKey();
CodegenModel cm = allModelsEntry.getValue();
CodegenModel parent = allModels.get(cm.getParent());
if (parent != null) {
if (parent.getDirectChildren() == null) {
parent.setDirectChildren(new ArrayList<>());
}
if (parent.getDirectChildren().stream().map(CodegenModel::getName)
.noneMatch(name -> name.equals(cm.getName()))) {
parent.getDirectChildren().add(cm);
parent.hasDirectChildren = true;
}
}
// if a discriminator exists on the parent, don't add this child to the inheritance hierarchy
// TODO Determine what to do if the parent discriminator name == the grandparent discriminator name
while (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(cm);
if (parent.getChildren().stream().map(CodegenModel::getName)
.noneMatch(name -> name.equals(cm.getName()))) {
parent.getChildren().add(cm);
}

parent.hasChildren = true;
Schema parentSchema = this.openAPI.getComponents().getSchemas().get(parent.schemaName);
if (parentSchema == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.VendorExtension;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures;
import org.openapitools.codegen.languages.features.OptionalFeatures;
Expand Down Expand Up @@ -113,6 +114,7 @@ public class SpringCodegen extends AbstractJavaCodegen
public static final String REQUEST_MAPPING_OPTION = "requestMappingMode";
public static final String USE_REQUEST_MAPPING_ON_CONTROLLER = "useRequestMappingOnController";
public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface";
public static final String USE_SEALED = "useSealed";

@Getter public enum RequestMappingMode {
api_interface("Generate the @RequestMapping annotation on the generated Api Interface."),
Expand Down Expand Up @@ -151,6 +153,7 @@ public class SpringCodegen extends AbstractJavaCodegen
protected boolean performBeanValidation = false;
@Setter protected boolean apiFirst = false;
protected boolean useOptional = false;
@Setter protected boolean useSealed = false;
@Setter protected boolean virtualService = false;
@Setter protected boolean hateoas = false;
@Setter protected boolean returnSuccessCode = false;
Expand Down Expand Up @@ -229,6 +232,8 @@ public SpringCodegen() {
.add(CliOption.newBoolean(USE_BEANVALIDATION, "Use BeanValidation API annotations", useBeanValidation));
cliOptions.add(CliOption.newBoolean(PERFORM_BEANVALIDATION,
"Use Bean Validation Impl. to perform BeanValidation", performBeanValidation));
cliOptions.add(CliOption.newBoolean(USE_SEALED,
"Whether to generate sealed model interfaces and classes"));
cliOptions.add(CliOption.newBoolean(API_FIRST,
"Generate the API from the OAI spec at server compile time (API first approach)", apiFirst));
cliOptions
Expand Down Expand Up @@ -423,6 +428,7 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(GENERATE_CONSTRUCTOR_WITH_REQUIRED_ARGS, value -> this.generatedConstructorWithRequiredArgs=value);
convertPropertyToBooleanAndWriteBack(RETURN_SUCCESS_CODE, this::setReturnSuccessCode);
convertPropertyToBooleanAndWriteBack(USE_SWAGGER_UI, this::setUseSwaggerUI);
convertPropertyToBooleanAndWriteBack(USE_SEALED, this::setUseSealed);
if (getDocumentationProvider().equals(DocumentationProvider.NONE)) {
this.setUseSwaggerUI(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{{>typeInfoAnnotation}}
{{/discriminator}}
{{>generatedAnnotation}}
public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
public {{#useSealed}}sealed {{/useSealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>sealed}}{
{{#discriminator}}
public {{propertyType}} {{propertyGetter}}();
{{/discriminator}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
{{#vendorExtensions.x-class-extra-annotation}}
{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
public {{#useSealed}}{{#hasDirectChildren}}sealed {{/hasDirectChildren}}{{^hasDirectChildren}}final {{/hasDirectChildren}}{{/useSealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{#useSealed}}{{#directChildren}}{{#-first}}permits {{/-first}}{{{classname}}}{{^-last}}, {{/-last}}{{/directChildren}} {{/useSealed}}{
{{#serializableModel}}

private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#useSealed}}{{#oneOf}}{{#-first}}permits {{/-first}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}{{#directChildren}}{{#-first}}{{#oneOf.isEmpty}}permits {{/oneOf.isEmpty}}{{/-first}}{{{classname}}}{{^-last}}, {{/-last}}{{/directChildren}} {{/useSealed}}
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,44 @@ public void testOneOfAndAllOf() throws IOException {
assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/PizzaSpeziale.java"), "import java.math.BigDecimal");
}

@Test
public void testSealedOneOfAndAllOf() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
String outputPath = output.getAbsolutePath().replace('\\', '/');
OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/oneof_polymorphism_and_inheritance.yaml", null, new ParseOptions()).getOpenAPI();

SpringCodegen codegen = new SpringCodegen();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true");
codegen.setUseOneOfInterfaces(true);
codegen.setUseSealed(true);

ClientOptInput input = new ClientOptInput();
input.openAPI(openAPI);
input.config(codegen);

DefaultGenerator generator = new DefaultGenerator();
codegen.setHateoas(true);
generator.setGenerateMetadata(false); // skip metadata and ↓ only generate models
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false");

codegen.setUseOneOfInterfaces(true);
codegen.setUseSealed(true);
codegen.setLegacyDiscriminatorBehavior(false);

generator.opts(input).generate();

assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/Foo.java"), "public final class Foo extends Entity implements FooRefOrValue");
assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/FooRef.java"), "public final class FooRef extends EntityRef implements FooRefOrValue");
assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/FooRefOrValue.java"), "public sealed interface FooRefOrValue permits Foo, FooRef ");
assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/Entity.java"), "public sealed class Entity extends RepresentationModel<Entity> permits Bar, BarCreate, Foo, Pasta, Pizza");
}

@Test
public void testDiscriminatorWithMappingIssue14731() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
Expand Down