Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/samples-jaxrs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- samples/server/petstore/jaxrs-spec-withxml
- samples/server/petstore/jaxrs-spec-interface
- samples/server/petstore/jaxrs-spec-interface-response
- samples/server/petstore/jaxrs-spec-oneof-interface
- samples/server/petstore/jaxrs-datelib-j8
- samples/server/petstore/jaxrs-resteasy/default
- samples/server/petstore/jaxrs-resteasy/eap
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/samples-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:
- samples/server/petstore/java-camel/**
- samples/server/petstore/java-helidon-server/v3/mp/**
- samples/server/petstore/java-helidon-server/v3/se/**
- samples/server/petstore/jaxrs-spec-sealed/**
pull_request:
paths:
# clients
Expand All @@ -44,6 +45,7 @@ on:
- samples/server/petstore/java-camel/**
- samples/server/petstore/java-helidon-server/v3/mp/**
- samples/server/petstore/java-helidon-server/v3/se/**
- samples/server/petstore/jaxrs-spec-sealed/**
jobs:
build:
name: Build with JDK17
Expand Down Expand Up @@ -72,6 +74,7 @@ jobs:
- samples/server/petstore/java-camel/
- samples/server/petstore/java-helidon-server/v3/mp/
- samples/server/petstore/java-helidon-server/v3/se
- samples/server/petstore/jaxrs-spec-sealed
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v5
Expand Down
9 changes: 9 additions & 0 deletions bin/configs/jaxrs-spec-oneof-interface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec-oneof-interface
inputSpec: modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec
additionalProperties:
artifactId: jaxrs-spec-oneof-interface-petstore-server
useOneOfInterfaces: "true"
serializableModel: "true"
hideGenerationTimestamp: "true"
10 changes: 10 additions & 0 deletions bin/configs/jaxrs-spec-sealed.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec-sealed
inputSpec: modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec
additionalProperties:
artifactId: jaxrs-spec-sealed-petstore-server
useOneOfInterfaces: "true"
useSealed: "true"
serializableModel: "true"
hideGenerationTimestamp: "true"
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.openapitools.codegen.model.OperationsMap;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand All @@ -56,6 +58,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
public static final String OPEN_API_SPEC_FILE_LOCATION = "openApiSpecFileLocation";
public static final String GENERATE_JSON_CREATOR = "generateJsonCreator";
public static final String USE_JAKARTA_SECURITY_ANNOTATIONS = "useJakartaSecurityAnnotations";
public static final String USE_SEALED = "useSealed";

public static final String QUARKUS_LIBRARY = "quarkus";
public static final String THORNTAIL_LIBRARY = "thorntail";
Expand All @@ -73,6 +76,9 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
private boolean useMutiny = false;
private boolean useJakartaSecurityAnnotations = false;

@Setter
protected boolean useSealed = false;

private final JakartaSecurityAnnotationProcessor jakartaSecurityAnnotationProcessor = new JakartaSecurityAnnotationProcessor();

@Getter @Setter
Expand Down Expand Up @@ -155,6 +161,7 @@ public JavaJAXRSSpecServerCodegen() {
cliOptions.add(CliOption.newBoolean(USE_MUTINY, "Whether to use Smallrye Mutiny instead of CompletionStage for asynchronous computation. Only valid when library is set to quarkus.", useMutiny));
cliOptions.add(CliOption.newBoolean(USE_JAKARTA_SECURITY_ANNOTATIONS, "Whether to generate Jakarta security annotations (@RolesAllowed, @PermitAll). Requires useJakartaEe=true. Currently only supported when library is set to quarkus.", useJakartaSecurityAnnotations));
cliOptions.add(CliOption.newBoolean(GENERATE_JSON_CREATOR, "Whether to generate @JsonCreator constructor for required properties.", generateJsonCreator));
cliOptions.add(CliOption.newBoolean(USE_SEALED, "Whether to generate sealed model interfaces and classes.", useSealed));
}

@Override
Expand Down Expand Up @@ -197,6 +204,7 @@ public void processOpts() {
}

convertPropertyToBooleanAndWriteBack(GENERATE_JSON_CREATOR, this::setGenerateJsonCreator);
convertPropertyToBooleanAndWriteBack(USE_SEALED, this::setUseSealed);

if (additionalProperties.containsKey(OPEN_API_SPEC_FILE_LOCATION)) {
openApiSpecFileLocation = additionalProperties.get(OPEN_API_SPEC_FILE_LOCATION).toString();
Expand Down Expand Up @@ -385,17 +393,34 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
Map<String, ModelsMap> result = super.postProcessAllModels(objs);

// Index the discriminators of the generated oneOf interfaces by classname. A child of a oneOf
// interface (useOneOfInterfaces) inherits a shared base via allOf and therefore has no Java
// parentModel carrying the discriminator - the discriminator lives on the interface it implements.
Map<String, CodegenDiscriminator> oneOfInterfaceDiscriminators = new HashMap<>();
for (ModelsMap modelsMap : result.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();
if (model.parentModel != null) {
CodegenDiscriminator discriminator = model.parentModel.getDiscriminator();
if (discriminator != null) {
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
if (mappedModel.getSchemaName().equals(model.schemaName)) {
model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName());
break;
}
if (Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface"))
&& model.getDiscriminator() != null) {
oneOfInterfaceDiscriminators.put(model.classname, model.getDiscriminator());
}
}
}

for (ModelsMap modelsMap : result.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();
// Resolve the @JsonTypeName mapping value from the Java parent's discriminator, if any,
// otherwise from the discriminator of the oneOf interface the model implements.
CodegenDiscriminator discriminator = model.parentModel != null
? model.parentModel.getDiscriminator()
: discriminatorOfImplementedOneOfInterface(model, oneOfInterfaceDiscriminators);
if (discriminator != null) {
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
if (mappedModel.getSchemaName().equals(model.schemaName)) {
model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName());
break;
}
}
}
Expand All @@ -404,6 +429,27 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
return result;
}

/**
* Returns the discriminator of the oneOf interface (produced by useOneOfInterfaces) that the given
* model implements, or {@code null} if the model does not implement such an interface. The interface
* name is read from the model's {@code x-implements} vendor extension.
*/
@SuppressWarnings("unchecked")
private CodegenDiscriminator discriminatorOfImplementedOneOfInterface(
CodegenModel model, Map<String, CodegenDiscriminator> oneOfInterfaceDiscriminators) {
Object implementsExtension = model.getVendorExtensions().get("x-implements");
if (!(implementsExtension instanceof Collection)) {
return null;
}
for (Object intf : (Collection<Object>) implementsExtension) {
CodegenDiscriminator discriminator = oneOfInterfaceDiscriminators.get(String.valueOf(intf));
if (discriminator != null) {
return discriminator;
}
}
return null;
}

@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{#additionalOneOfTypeAnnotations}}{{{.}}}
{{/additionalOneOfTypeAnnotations}}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ import {{javaxPackage}}.validation.Valid;
{{>enumOuterClass}}

{{/isEnum}}
{{^isEnum}}{{>pojo}}{{/isEnum}}
{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>pojo}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}}
{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{>additionalOneOfTypeAnnotations}}
{{#discriminator}}
{{>typeInfoAnnotation}}

{{/discriminator}}{{^discriminator}}{{#useDeductionForOneOfInterfaces}}
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
{{#interfaceModels}}
@JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}}
{{/interfaceModels}}
})
{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-class-extra-annotation}}{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
{{/discriminator}}
{{>generatedAnnotation}}

public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
{{#discriminator}}
{{propertyType}} {{propertyGetter}}();
{{/discriminator}}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#useSealed}}{{#permits}}{{#-first}}permits {{/-first}}{{{.}}}{{^-last}}, {{/-last}}{{#-last}} {{/-last}}{{/permits}}{{/useSealed}}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {{javaxPackage}}.xml.bind.annotation.XmlEnumValue;
{{#vendorExtensions.x-class-extra-annotation}}
{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
{{#vars}}
{{#isEnum}}
{{^isContainer}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
{{/openApiNullable}}
</dependencies>
<properties>
<java.version>1.8</java.version>
<java.version>{{#useSealed}}17{{/useSealed}}{{^useSealed}}1.8{{/useSealed}}</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +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}}
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,60 @@ public void testDiscriminatorJsonIgnorePropertiesPropagatesToChildren_whenLegacy
}
}

/**
* With {@code useOneOfInterfaces=true} a oneOf schema is generated as a Java interface, and the
* concrete subtypes implement it. With {@code useSealed=true} the interface is {@code sealed} and
* {@code permits} its subtypes, which become {@code final} and {@code implements} the interface.
* The discriminator property is declared only on a shared non-discriminator base (acyclic pattern),
* so the interface getter type must resolve to the enum model (PetType) from the mapped children.
*/
@Test
public void testOneOfSealedInterfaceGeneration() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Map<String, Object> properties = new HashMap<>();
properties.put("useOneOfInterfaces", "true");
properties.put("useSealed", "true");

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("jaxrs-spec")
.setAdditionalProperties(properties)
.setInputSpec("src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.opts(configurator.toClientOptInput()).generate();

// assertFileContains is used rather than JavaFileAssert because the latter parses the source
// with a JavaParser language level that predates sealed types and would reject the `sealed`
// and `permits` keywords.
String modelDir = output.getAbsolutePath().replace("\\", "/") + "/src/gen/java/org/openapitools/model/";

// The oneOf schema becomes a sealed interface that permits its subtypes, and declares the
// discriminator getter with the resolved enum type rather than String.
assertFileContains(Paths.get(modelDir + "PetRequest.java"),
"public sealed interface PetRequest",
"permits CatRequest, DogRequest",
"PetType getPetType();");
// an interface is not a class and must not extend a parent
assertFileNotContains(Paths.get(modelDir + "PetRequest.java"), "class PetRequest");

// The concrete subtypes are final and implement (not extend) the interface, with a matching
// getter return type, so there is no cyclical extends/implements and no return-type clash.
assertFileContains(Paths.get(modelDir + "CatRequest.java"),
"public final class CatRequest",
"implements PetRequest",
"public PetType getPetType()");
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "extends PetRequest");

assertFileContains(Paths.get(modelDir + "DogRequest.java"),
"public final class DogRequest",
"implements PetRequest",
"public PetType getPetType()");
assertFileNotContains(Paths.get(modelDir + "DogRequest.java"), "extends PetRequest");
}

@Test
public void testGenerateJsonNullableListFieldsHelperMethodReferences_issue23251() throws Exception {
Map<String, Object> properties = new HashMap<>();
Expand Down Expand Up @@ -2106,4 +2160,60 @@ private String generateQuarkusItemsApi(String specPath, boolean interfaceOnly, b
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java");
return Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java"));
}

/**
* With {@code useOneOfInterfaces=true} a oneOf schema is generated as a Java interface, and the
* concrete subtypes implement (not extend) it. The discriminator property is declared only on a
* shared non-discriminator base (acyclic pattern), so the interface getter type resolves to the
* enum model (PetType) from the mapped children rather than falling back to String.
*/
@Test
public void testOneOfInterfaceGeneration() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Map<String, Object> properties = new HashMap<>();
properties.put("useOneOfInterfaces", "true");

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("jaxrs-spec")
.setAdditionalProperties(properties)
.setInputSpec("src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.opts(configurator.toClientOptInput()).generate();

String modelDir = output.getAbsolutePath().replace("\\", "/") + "/src/gen/java/org/openapitools/model/";

// The oneOf schema becomes an interface declaring the discriminator getter with the resolved
// enum type rather than String.
assertFileContains(Paths.get(modelDir + "PetRequest.java"),
"public interface PetRequest",
"PetType getPetType();");
// an interface is not a class and must not extend a parent
assertFileNotContains(Paths.get(modelDir + "PetRequest.java"), "class PetRequest");

// The concrete subtypes implement (not extend) the interface, with a matching getter return type,
// so there is no cyclical extends/implements and no return-type clash.
assertFileContains(Paths.get(modelDir + "CatRequest.java"),
"public class CatRequest",
"implements PetRequest",
"public PetType getPetType()");
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "extends PetRequest");

assertFileContains(Paths.get(modelDir + "DogRequest.java"),
"public class DogRequest",
"implements PetRequest",
"public PetType getPetType()");
assertFileNotContains(Paths.get(modelDir + "DogRequest.java"), "extends PetRequest");

// The @JsonTypeName of each subtype is the discriminator mapping value (CAT/DOG), resolved from
// the oneOf interface it implements, not the class name - so polymorphic (de)serialization keys
// off the discriminator value and round-trips with the @JsonSubTypes mapping on the interface.
assertFileContains(Paths.get(modelDir + "CatRequest.java"), "@JsonTypeName(\"CAT\")");
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "@JsonTypeName(\"CatRequest\")");
assertFileContains(Paths.get(modelDir + "DogRequest.java"), "@JsonTypeName(\"DOG\")");
assertFileNotContains(Paths.get(modelDir + "DogRequest.java"), "@JsonTypeName(\"DogRequest\")");
}
}
Loading
Loading