Skip to content

Commit cd7cdb1

Browse files
alex-ntmartin-mfg
andauthored
Add option to generate a fully sealed model in the JavaSpring generator (#20503)
* Generated sealed interfaces for oneOf * Add generated data * Add also modifier * Allow sealed for everything * Fully seal model * Disable html escaping * Update modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java Co-authored-by: martin-mfg <[email protected]> * Update docs * Check all oneOf scenarios * Fix failed scenario * Fix comments * Remove unused import * Adapt pom.xml also * Add comment and remove unused function --------- Co-authored-by: martin-mfg <[email protected]>
1 parent d5866fe commit cd7cdb1

File tree

14 files changed

+142
-12
lines changed

14 files changed

+142
-12
lines changed

docs/generators/java-camel.md

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
103103
|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|
104104
|useOptional|Use Optional container for optional parameters| |false|
105105
|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|
106+
|useSealed|Whether to generate sealed model interfaces and classes| |false|
106107
|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|
107108
|useSpringController|Annotate the generated API as a Spring Controller| |false|
108109
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|

docs/generators/spring.md

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
9696
|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|
9797
|useOptional|Use Optional container for optional parameters| |false|
9898
|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|
99+
|useSealed|Whether to generate sealed model interfaces and classes| |false|
99100
|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|
100101
|useSpringController|Annotate the generated API as a Spring Controller| |false|
101102
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|

flake.lock

+24-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
5959
public Set<String> oneOf = new TreeSet<>();
6060
public Set<String> allOf = new TreeSet<>();
6161

62+
// direct descendants that are allowed to extend the current model
63+
public List<String> permits = new ArrayList<>();
64+
6265
// The schema name as written in the OpenAPI document
6366
// If it's a reserved word, it will be escaped.
6467
@Getter @Setter
@@ -922,6 +925,7 @@ public boolean equals(Object o) {
922925
Objects.equals(parentModel, that.parentModel) &&
923926
Objects.equals(interfaceModels, that.interfaceModels) &&
924927
Objects.equals(children, that.children) &&
928+
Objects.equals(permits, that.permits) &&
925929
Objects.equals(anyOf, that.anyOf) &&
926930
Objects.equals(oneOf, that.oneOf) &&
927931
Objects.equals(allOf, that.allOf) &&
@@ -975,7 +979,7 @@ public boolean equals(Object o) {
975979
@Override
976980
public int hashCode() {
977981
return Objects.hash(getParent(), getParentSchema(), getInterfaces(), getAllParents(), getParentModel(),
978-
getInterfaceModels(), getChildren(), anyOf, oneOf, allOf, getName(), getSchemaName(), getClassname(), getTitle(),
982+
getInterfaceModels(), getChildren(), permits, anyOf, oneOf, allOf, getName(), getSchemaName(), getClassname(), getTitle(),
979983
getDescription(), getClassVarName(), getModelJson(), getDataType(), getXmlPrefix(), getXmlNamespace(),
980984
getXmlName(), getClassFilename(), getUnescapedDescription(), getDiscriminator(), getDefaultValue(),
981985
getArrayModelType(), isAlias, isString, isInteger, isLong, isNumber, isNumeric, isFloat, isDouble,
@@ -1005,6 +1009,7 @@ public String toString() {
10051009
sb.append(", allParents=").append(allParents);
10061010
sb.append(", parentModel=").append(parentModel);
10071011
sb.append(", children=").append(children != null ? children.size() : "[]");
1012+
sb.append(", permits=").append(permits != null ? permits.size() : "[]");
10081013
sb.append(", anyOf=").append(anyOf);
10091014
sb.append(", oneOf=").append(oneOf);
10101015
sb.append(", allOf=").append(allOf);

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java

+14-3
Original file line numberDiff line numberDiff line change
@@ -621,16 +621,25 @@ public Map<String, ModelsMap> updateAllModels(Map<String, ModelsMap> objs) {
621621

622622
// Let parent know about all its children
623623
for (Map.Entry<String, CodegenModel> allModelsEntry : allModels.entrySet()) {
624-
String name = allModelsEntry.getKey();
625624
CodegenModel cm = allModelsEntry.getValue();
626625
CodegenModel parent = allModels.get(cm.getParent());
626+
if (parent != null) {
627+
if (!parent.permits.contains(cm.classname) && parent.permits.stream()
628+
.noneMatch(name -> name.equals(cm.getName()))) {
629+
parent.permits.add(cm.classname);
630+
}
631+
}
627632
// if a discriminator exists on the parent, don't add this child to the inheritance hierarchy
628633
// TODO Determine what to do if the parent discriminator name == the grandparent discriminator name
629634
while (parent != null) {
630635
if (parent.getChildren() == null) {
631636
parent.setChildren(new ArrayList<>());
632637
}
633-
parent.getChildren().add(cm);
638+
if (parent.getChildren().stream().map(CodegenModel::getName)
639+
.noneMatch(name -> name.equals(cm.getName()))) {
640+
parent.getChildren().add(cm);
641+
}
642+
634643
parent.hasChildren = true;
635644
Schema parentSchema = this.openAPI.getComponents().getSchemas().get(parent.schemaName);
636645
if (parentSchema == null) {
@@ -2704,7 +2713,6 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
27042713
LOGGER.debug("{} (anyOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType);
27052714
} else {
27062715
m.anyOf.add(languageType);
2707-
27082716
}
27092717
} else if (composed.getOneOf() != null) {
27102718
if (m.oneOf.contains(languageType)) {
@@ -2751,6 +2759,9 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
27512759
m.anyOf.add(modelName);
27522760
} else if (composed.getOneOf() != null) {
27532761
m.oneOf.add(modelName);
2762+
if (!m.permits.contains(modelName)) {
2763+
m.permits.add(modelName);
2764+
}
27542765
} else if (composed.getAllOf() != null) {
27552766
m.allOf.add(modelName);
27562767
} else {

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java

+5
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public class SpringCodegen extends AbstractJavaCodegen
113113
public static final String REQUEST_MAPPING_OPTION = "requestMappingMode";
114114
public static final String USE_REQUEST_MAPPING_ON_CONTROLLER = "useRequestMappingOnController";
115115
public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface";
116+
public static final String USE_SEALED = "useSealed";
116117

117118
@Getter public enum RequestMappingMode {
118119
api_interface("Generate the @RequestMapping annotation on the generated Api Interface."),
@@ -151,6 +152,7 @@ public class SpringCodegen extends AbstractJavaCodegen
151152
protected boolean performBeanValidation = false;
152153
@Setter protected boolean apiFirst = false;
153154
protected boolean useOptional = false;
155+
@Setter protected boolean useSealed = false;
154156
@Setter protected boolean virtualService = false;
155157
@Setter protected boolean hateoas = false;
156158
@Setter protected boolean returnSuccessCode = false;
@@ -229,6 +231,8 @@ public SpringCodegen() {
229231
.add(CliOption.newBoolean(USE_BEANVALIDATION, "Use BeanValidation API annotations", useBeanValidation));
230232
cliOptions.add(CliOption.newBoolean(PERFORM_BEANVALIDATION,
231233
"Use Bean Validation Impl. to perform BeanValidation", performBeanValidation));
234+
cliOptions.add(CliOption.newBoolean(USE_SEALED,
235+
"Whether to generate sealed model interfaces and classes"));
232236
cliOptions.add(CliOption.newBoolean(API_FIRST,
233237
"Generate the API from the OAI spec at server compile time (API first approach)", apiFirst));
234238
cliOptions
@@ -423,6 +427,7 @@ public void processOpts() {
423427
convertPropertyToBooleanAndWriteBack(GENERATE_CONSTRUCTOR_WITH_REQUIRED_ARGS, value -> this.generatedConstructorWithRequiredArgs=value);
424428
convertPropertyToBooleanAndWriteBack(RETURN_SUCCESS_CODE, this::setReturnSuccessCode);
425429
convertPropertyToBooleanAndWriteBack(USE_SWAGGER_UI, this::setUseSwaggerUI);
430+
convertPropertyToBooleanAndWriteBack(USE_SEALED, this::setUseSealed);
426431
if (getDocumentationProvider().equals(DocumentationProvider.NONE)) {
427432
this.setUseSwaggerUI(false);
428433
}

modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache

+5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
<name>{{artifactId}}</name>
77
<version>{{artifactVersion}}</version>
88
<properties>
9+
{{#useSealed}}
10+
<java.version>17</java.version>
11+
{{/useSealed}}
12+
{{^useSealed}}
913
<java.version>1.8</java.version>
14+
{{/useSealed}}
1015
<maven.compiler.source>${java.version}</maven.compiler.source>
1116
<maven.compiler.target>${java.version}</maven.compiler.target>
1217
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-cloud/pom-sb3.mustache

+5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
<name>{{artifactId}}</name>
77
<version>{{artifactVersion}}</version>
88
<properties>
9+
{{#useSealed}}
10+
<java.version>17</java.version>
11+
{{/useSealed}}
12+
{{^useSealed}}
913
<java.version>8</java.version>
14+
{{/useSealed}}
1015
<maven.compiler.source>${java.version}</maven.compiler.source>
1116
<maven.compiler.target>${java.version}</maven.compiler.target>
1217
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-cloud/pom.mustache

+5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
<name>{{artifactId}}</name>
77
<version>{{artifactVersion}}</version>
88
<properties>
9+
{{#useSealed}}
10+
<java.version>17</java.version>
11+
{{/useSealed}}
12+
{{^useSealed}}
913
<java.version>1.8</java.version>
14+
{{/useSealed}}
1015
<maven.compiler.source>${java.version}</maven.compiler.source>
1116
<maven.compiler.target>${java.version}</maven.compiler.target>
1217
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{{>typeInfoAnnotation}}
77
{{/discriminator}}
88
{{>generatedAnnotation}}
9-
public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
9+
public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
1010
{{#discriminator}}
1111
public {{propertyType}} {{propertyGetter}}();
1212
{{/discriminator}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{#useSealed}}{{#permits}}{{#-first}}permits {{/-first}}{{{.}}}{{^-last}}, {{/-last}}{{/permits}} {{/useSealed}}

modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
{{#vendorExtensions.x-class-extra-annotation}}
3232
{{{vendorExtensions.x-class-extra-annotation}}}
3333
{{/vendorExtensions.x-class-extra-annotation}}
34-
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}} {
34+
public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
3535
{{#serializableModel}}
3636

3737
private static final long serialVersionUID = 1L;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{#useSealed}}{{#permits.0}}sealed {{/permits.0}}{{^permits.0}}{{^vendorExtensions.x-is-one-of-interface}}final {{/vendorExtensions.x-is-one-of-interface}}{{/permits.0}}{{/useSealed}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java

+72
Original file line numberDiff line numberDiff line change
@@ -2259,6 +2259,78 @@ public void paramPageableIsNotSpringPaginated_issue13052() throws Exception {
22592259
.assertParameter("pageable").hasType("Pageable");
22602260
}
22612261

2262+
@DataProvider(name = "sealedScenarios")
2263+
public static Object[][] sealedScenarios() {
2264+
return new Object[][]{
2265+
{"oneof_polymorphism_and_inheritance.yaml", Map.of(
2266+
"Foo.java", "public final class Foo extends Entity implements FooRefOrValue",
2267+
"FooRef.java", "public final class FooRef extends EntityRef implements FooRefOrValue",
2268+
"FooRefOrValue.java", "public sealed interface FooRefOrValue permits Foo, FooRef ",
2269+
"Entity.java", "public sealed class Entity extends RepresentationModel<Entity> permits Bar, BarCreate, Foo, Pasta, Pizza {")},
2270+
{"oneOf_additionalProperties.yaml", Map.of(
2271+
"SchemaA.java", "public final class SchemaA extends RepresentationModel<SchemaA> implements PostRequest {",
2272+
"PostRequest.java", "public sealed interface PostRequest permits SchemaA {")},
2273+
{"oneOf_array.yaml", Map.of(
2274+
"MyExampleGet200Response.java", "public interface MyExampleGet200Response")},
2275+
{"oneOf_duplicateArray.yaml", Map.of(
2276+
"Example.java", "public interface Example {")},
2277+
{"oneOf_nonPrimitive.yaml", Map.of(
2278+
"Example.java", "public interface Example {")},
2279+
{"oneOf_primitive.yaml", Map.of(
2280+
"Child.java", "public final class Child extends RepresentationModel<Child> implements Example {",
2281+
"Example.java", "public sealed interface Example permits Child {")},
2282+
{"oneOf_primitiveAndArray.yaml", Map.of(
2283+
"Example.java", "public interface Example {")},
2284+
{"oneOf_reuseRef.yaml", Map.of(
2285+
"Fruit.java", "public sealed interface Fruit permits Apple, Banana {",
2286+
"Banana.java", "public final class Banana extends RepresentationModel<Banana> implements Fruit {",
2287+
"Apple.java", "public final class Apple extends RepresentationModel<Apple> implements Fruit {")},
2288+
{"oneOf_twoPrimitives.yaml", Map.of(
2289+
"MyExamplePostRequest.java", "public interface MyExamplePostRequest {")},
2290+
{"oneOfArrayMapImport.yaml", Map.of(
2291+
"Fruit.java", "public interface Fruit {",
2292+
"Grape.java", "public final class Grape extends RepresentationModel<Grape> {",
2293+
"Apple.java", "public final class Apple extends RepresentationModel<Apple> {")},
2294+
{"oneOfDiscriminator.yaml", Map.of(
2295+
"FruitAllOfDisc.java", "public sealed interface FruitAllOfDisc permits AppleAllOfDisc, BananaAllOfDisc {",
2296+
"FruitReqDisc.java", "public sealed interface FruitReqDisc permits AppleReqDisc, BananaReqDisc {\n")}
2297+
};
2298+
}
2299+
2300+
@Test(dataProvider = "sealedScenarios", description = "sealed scenarios")
2301+
public void sealedScenarios(String apiFile, Map<String, String> definitions) throws IOException {
2302+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
2303+
output.deleteOnExit();
2304+
String outputPath = output.getAbsolutePath().replace('\\', '/');
2305+
OpenAPI openAPI = new OpenAPIParser()
2306+
.readLocation("src/test/resources/3_0/" + apiFile, null, new ParseOptions()).getOpenAPI();
2307+
2308+
SpringCodegen codegen = new SpringCodegen();
2309+
codegen.setOutputDir(output.getAbsolutePath());
2310+
codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true");
2311+
codegen.setUseOneOfInterfaces(true);
2312+
codegen.setUseSealed(true);
2313+
2314+
ClientOptInput input = new ClientOptInput();
2315+
input.openAPI(openAPI);
2316+
input.config(codegen);
2317+
2318+
DefaultGenerator generator = new DefaultGenerator();
2319+
codegen.setHateoas(true);
2320+
generator.setGenerateMetadata(false); // skip metadata and ↓ only generate models
2321+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
2322+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
2323+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
2324+
generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false");
2325+
2326+
codegen.setLegacyDiscriminatorBehavior(false);
2327+
2328+
generator.opts(input).generate();
2329+
2330+
definitions.forEach((file, check) ->
2331+
assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/" + file), check));
2332+
}
2333+
22622334
@Test
22632335
public void shouldSetDefaultValueForMultipleArrayItems() throws IOException {
22642336
Map<String, Object> additionalProperties = new HashMap<>();

0 commit comments

Comments
 (0)