Skip to content

Commit 1aca0cf

Browse files
[jaxrs-spec] Support useOneOfInterfaces (#24174)
The jaxrs-spec generator had no rendering for the useOneOfInterfaces feature: a oneOf schema was always emitted as a pojo, even when the feature transformed it into an interface model. This adds the missing interface template so that, with useOneOfInterfaces=true, a oneOf schema is generated as a Java interface and the concrete subtypes implement it. - model.mustache routes x-is-one-of-interface models to a new oneof_interface template; all other models still render as pojos (default behavior unchanged). - oneof_interface.mustache emits a plain interface, carrying the discriminator getter (whose type now resolves to the shared enum, e.g. PetType) plus the generated/typeInfo/deduction annotations. - Adds the oneof_interface fixture, a JavaJAXRSSpecServerCodegenTest, and a jaxrs-spec-oneof-interface sample (registered in the JAX-RS samples workflow). The feature stays opt-in; default jaxrs-spec output is unchanged.
1 parent e7b9488 commit 1aca0cf

22 files changed

Lines changed: 1039 additions & 9 deletions

File tree

‎.github/workflows/samples-jaxrs.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
- samples/server/petstore/jaxrs-spec-withxml
2424
- samples/server/petstore/jaxrs-spec-interface
2525
- samples/server/petstore/jaxrs-spec-interface-response
26+
- samples/server/petstore/jaxrs-spec-oneof-interface
2627
- samples/server/petstore/jaxrs-datelib-j8
2728
- samples/server/petstore/jaxrs-resteasy/default
2829
- samples/server/petstore/jaxrs-resteasy/eap
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
generatorName: jaxrs-spec
2+
outputDir: samples/server/petstore/jaxrs-spec-oneof-interface
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec
5+
additionalProperties:
6+
artifactId: jaxrs-spec-oneof-interface-petstore-server
7+
useOneOfInterfaces: "true"
8+
serializableModel: "true"
9+
hideGenerationTimestamp: "true"

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

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.openapitools.codegen.model.OperationsMap;
3333

3434
import java.io.File;
35+
import java.util.Collection;
36+
import java.util.HashMap;
3537
import java.util.List;
3638
import java.util.Map;
3739

@@ -391,17 +393,34 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
391393
@Override
392394
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
393395
Map<String, ModelsMap> result = super.postProcessAllModels(objs);
396+
397+
// Index the discriminators of the generated oneOf interfaces by classname. A child of a oneOf
398+
// interface (useOneOfInterfaces) inherits a shared base via allOf and therefore has no Java
399+
// parentModel carrying the discriminator - the discriminator lives on the interface it implements.
400+
Map<String, CodegenDiscriminator> oneOfInterfaceDiscriminators = new HashMap<>();
401+
for (ModelsMap modelsMap : result.values()) {
402+
for (ModelMap modelMap : modelsMap.getModels()) {
403+
CodegenModel model = modelMap.getModel();
404+
if (Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface"))
405+
&& model.getDiscriminator() != null) {
406+
oneOfInterfaceDiscriminators.put(model.classname, model.getDiscriminator());
407+
}
408+
}
409+
}
410+
394411
for (ModelsMap modelsMap : result.values()) {
395412
for (ModelMap modelMap : modelsMap.getModels()) {
396413
CodegenModel model = modelMap.getModel();
397-
if (model.parentModel != null) {
398-
CodegenDiscriminator discriminator = model.parentModel.getDiscriminator();
399-
if (discriminator != null) {
400-
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
401-
if (mappedModel.getSchemaName().equals(model.schemaName)) {
402-
model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName());
403-
break;
404-
}
414+
// Resolve the @JsonTypeName mapping value from the Java parent's discriminator, if any,
415+
// otherwise from the discriminator of the oneOf interface the model implements.
416+
CodegenDiscriminator discriminator = model.parentModel != null
417+
? model.parentModel.getDiscriminator()
418+
: discriminatorOfImplementedOneOfInterface(model, oneOfInterfaceDiscriminators);
419+
if (discriminator != null) {
420+
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
421+
if (mappedModel.getSchemaName().equals(model.schemaName)) {
422+
model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName());
423+
break;
405424
}
406425
}
407426
}
@@ -410,6 +429,27 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
410429
return result;
411430
}
412431

432+
/**
433+
* Returns the discriminator of the oneOf interface (produced by useOneOfInterfaces) that the given
434+
* model implements, or {@code null} if the model does not implement such an interface. The interface
435+
* name is read from the model's {@code x-implements} vendor extension.
436+
*/
437+
@SuppressWarnings("unchecked")
438+
private CodegenDiscriminator discriminatorOfImplementedOneOfInterface(
439+
CodegenModel model, Map<String, CodegenDiscriminator> oneOfInterfaceDiscriminators) {
440+
Object implementsExtension = model.getVendorExtensions().get("x-implements");
441+
if (!(implementsExtension instanceof Collection)) {
442+
return null;
443+
}
444+
for (Object intf : (Collection<Object>) implementsExtension) {
445+
CodegenDiscriminator discriminator = oneOfInterfaceDiscriminators.get(String.valueOf(intf));
446+
if (discriminator != null) {
447+
return discriminator;
448+
}
449+
}
450+
return null;
451+
}
452+
413453
@Override
414454
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
415455
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{{#additionalOneOfTypeAnnotations}}{{{.}}}
2+
{{/additionalOneOfTypeAnnotations}}

‎modules/openapi-generator/src/main/resources/JavaJaxRS/spec/model.mustache‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ import {{javaxPackage}}.validation.Valid;
1616
{{>enumOuterClass}}
1717

1818
{{/isEnum}}
19-
{{^isEnum}}{{>pojo}}{{/isEnum}}
19+
{{^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}}
2020
{{/model}}
2121
{{/models}}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{{>additionalOneOfTypeAnnotations}}
2+
{{#discriminator}}
3+
{{>typeInfoAnnotation}}
4+
5+
{{/discriminator}}{{^discriminator}}{{#useDeductionForOneOfInterfaces}}
6+
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
7+
@JsonSubTypes({
8+
{{#interfaceModels}}
9+
@JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}}
10+
{{/interfaceModels}}
11+
})
12+
{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-class-extra-annotation}}{{{vendorExtensions.x-class-extra-annotation}}}
13+
{{/vendorExtensions.x-class-extra-annotation}}
14+
{{/discriminator}}
15+
{{>generatedAnnotation}}
16+
17+
public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
18+
{{#discriminator}}
19+
{{propertyType}} {{propertyGetter}}();
20+
{{/discriminator}}
21+
}

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2106,4 +2106,60 @@ private String generateQuarkusItemsApi(String specPath, boolean interfaceOnly, b
21062106
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java");
21072107
return Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java"));
21082108
}
2109+
2110+
/**
2111+
* With {@code useOneOfInterfaces=true} a oneOf schema is generated as a Java interface, and the
2112+
* concrete subtypes implement (not extend) it. The discriminator property is declared only on a
2113+
* shared non-discriminator base (acyclic pattern), so the interface getter type resolves to the
2114+
* enum model (PetType) from the mapped children rather than falling back to String.
2115+
*/
2116+
@Test
2117+
public void testOneOfInterfaceGeneration() throws Exception {
2118+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
2119+
output.deleteOnExit();
2120+
2121+
Map<String, Object> properties = new HashMap<>();
2122+
properties.put("useOneOfInterfaces", "true");
2123+
2124+
final CodegenConfigurator configurator = new CodegenConfigurator()
2125+
.setGeneratorName("jaxrs-spec")
2126+
.setAdditionalProperties(properties)
2127+
.setInputSpec("src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml")
2128+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
2129+
2130+
DefaultGenerator generator = new DefaultGenerator();
2131+
generator.opts(configurator.toClientOptInput()).generate();
2132+
2133+
String modelDir = output.getAbsolutePath().replace("\\", "/") + "/src/gen/java/org/openapitools/model/";
2134+
2135+
// The oneOf schema becomes an interface declaring the discriminator getter with the resolved
2136+
// enum type rather than String.
2137+
assertFileContains(Paths.get(modelDir + "PetRequest.java"),
2138+
"public interface PetRequest",
2139+
"PetType getPetType();");
2140+
// an interface is not a class and must not extend a parent
2141+
assertFileNotContains(Paths.get(modelDir + "PetRequest.java"), "class PetRequest");
2142+
2143+
// The concrete subtypes implement (not extend) the interface, with a matching getter return type,
2144+
// so there is no cyclical extends/implements and no return-type clash.
2145+
assertFileContains(Paths.get(modelDir + "CatRequest.java"),
2146+
"public class CatRequest",
2147+
"implements PetRequest",
2148+
"public PetType getPetType()");
2149+
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "extends PetRequest");
2150+
2151+
assertFileContains(Paths.get(modelDir + "DogRequest.java"),
2152+
"public class DogRequest",
2153+
"implements PetRequest",
2154+
"public PetType getPetType()");
2155+
assertFileNotContains(Paths.get(modelDir + "DogRequest.java"), "extends PetRequest");
2156+
2157+
// The @JsonTypeName of each subtype is the discriminator mapping value (CAT/DOG), resolved from
2158+
// the oneOf interface it implements, not the class name - so polymorphic (de)serialization keys
2159+
// off the discriminator value and round-trips with the @JsonSubTypes mapping on the interface.
2160+
assertFileContains(Paths.get(modelDir + "CatRequest.java"), "@JsonTypeName(\"CAT\")");
2161+
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "@JsonTypeName(\"CatRequest\")");
2162+
assertFileContains(Paths.get(modelDir + "DogRequest.java"), "@JsonTypeName(\"DOG\")");
2163+
assertFileNotContains(Paths.get(modelDir + "DogRequest.java"), "@JsonTypeName(\"DogRequest\")");
2164+
}
21092165
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
openapi: 3.0.3
2+
info:
3+
title: oneOf interface
4+
description: >
5+
A oneOf interface whose subtypes inherit shared properties from a base schema
6+
(acyclic pattern). The discriminator property is declared on the oneOf container
7+
and on the shared base the subtypes inherit via allOf, so the interface getter
8+
type resolves to the enum from the container's own properties.
9+
version: 1.0.0
10+
paths:
11+
/pets:
12+
post:
13+
operationId: createPet
14+
requestBody:
15+
required: true
16+
content:
17+
application/json:
18+
schema:
19+
$ref: '#/components/schemas/PetRequest'
20+
responses:
21+
'201':
22+
description: created
23+
components:
24+
schemas:
25+
PetType:
26+
type: string
27+
enum:
28+
- CAT
29+
- DOG
30+
description: Discriminator value identifying the type of pet
31+
32+
PetRequest:
33+
type: object
34+
properties:
35+
petType:
36+
$ref: '#/components/schemas/PetType'
37+
oneOf:
38+
- $ref: '#/components/schemas/CatRequest'
39+
- $ref: '#/components/schemas/DogRequest'
40+
discriminator:
41+
propertyName: petType
42+
mapping:
43+
CAT: '#/components/schemas/CatRequest'
44+
DOG: '#/components/schemas/DogRequest'
45+
46+
PetBase:
47+
type: object
48+
required:
49+
- petType
50+
- name
51+
properties:
52+
petType:
53+
$ref: '#/components/schemas/PetType'
54+
name:
55+
type: string
56+
57+
CatRequest:
58+
type: object
59+
allOf:
60+
- $ref: '#/components/schemas/PetBase'
61+
- type: object
62+
required:
63+
- indoor
64+
properties:
65+
indoor:
66+
type: boolean
67+
68+
DogRequest:
69+
type: object
70+
allOf:
71+
- $ref: '#/components/schemas/PetBase'
72+
- type: object
73+
required:
74+
- trained
75+
properties:
76+
trained:
77+
type: boolean
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# OpenAPI Generator Ignore
2+
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
3+
4+
# Use this file to prevent files from being overwritten by the generator.
5+
# The patterns follow closely to .gitignore or .dockerignore.
6+
7+
# As an example, the C# client generator defines ApiClient.cs.
8+
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
9+
#ApiClient.cs
10+
11+
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
12+
#foo/*/qux
13+
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
14+
15+
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
16+
#foo/**/qux
17+
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
18+
19+
# You can also negate patterns with an exclamation (!).
20+
# For example, you can ignore all files in a docs folder with the file extension .md:
21+
#docs/*.md
22+
# Then explicitly reverse the ignore rule for a single file:
23+
#!docs/README.md
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
README.md
2+
pom.xml
3+
src/gen/java/org/openapitools/api/PetsApi.java
4+
src/gen/java/org/openapitools/api/RestApplication.java
5+
src/gen/java/org/openapitools/api/RestResourceRoot.java
6+
src/gen/java/org/openapitools/model/CatRequest.java
7+
src/gen/java/org/openapitools/model/DogRequest.java
8+
src/gen/java/org/openapitools/model/PetBase.java
9+
src/gen/java/org/openapitools/model/PetRequest.java
10+
src/gen/java/org/openapitools/model/PetType.java
11+
src/main/openapi/openapi.yaml

0 commit comments

Comments
 (0)