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
7 changes: 6 additions & 1 deletion .github/workflows/samples-jaxrs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
- samples/server/petstore/jaxrs-spec-interface
- samples/server/petstore/jaxrs-spec-interface-response
- samples/server/petstore/jaxrs-spec-oneof-interface
- samples/server/petstore/jaxrs-spec-jsonb
- samples/server/petstore/jaxrs-datelib-j8
- samples/server/petstore/jaxrs-resteasy/default
- samples/server/petstore/jaxrs-resteasy/eap
Expand All @@ -38,6 +39,10 @@ jobs:
- samples/server/petstore/jaxrs-spec-swagger-annotations
- samples/server/petstore/jaxrs-spec-swagger-v3-annotations-jakarta
- samples/server/petstore/jaxrs-spec-swagger-v3-annotations
include:
# quarkus-maven-plugin 1.13.x needs Maven Resolver 1.x, which Maven 3.10 no longer ships
- sample: samples/server/petstore/jaxrs-spec-microprofile-openapi-annotations
mvn: $GITHUB_WORKSPACE/mvnw
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
Expand All @@ -54,4 +59,4 @@ jobs:
key: ${{ runner.os }}-${{ github.job }}-${{ env.cache-name }}-${{ hashFiles('**/pom.xml') }}
- name: Build
working-directory: ${{ matrix.sample }}
run: mvn clean package --no-transfer-progress
run: ${{ matrix.mvn || 'mvn' }} clean package --no-transfer-progress
12 changes: 12 additions & 0 deletions bin/configs/jaxrs-spec-jsonb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec-jsonb
inputSpec: modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/petstore-with-fake-endpoints-models-for-testing.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec
additionalProperties:
artifactId: jaxrs-spec-jsonb-petstore-server
serializationLibrary: jsonb
useJakartaEe: "true"
serializableModel: "true"
hideGenerationTimestamp: "true"
implicitHeadersRegex: (api_key|enum_header_string)
generateBuilders: "true"
3 changes: 2 additions & 1 deletion docs/generators/jaxrs-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response. With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|<dl><dt>**false**</dt><dd>No changes to the enums are made, this is the default option.</dd><dt>**true**</dt><dd>With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.</dd></dl>|false|
|generateBuilders|Whether to generate builders for models| |false|
|generateConstructorWithAllArgs|whether to generate a constructor for all arguments| |false|
|generateJsonCreator|Whether to generate @JsonCreator constructor for required properties.| |true|
|generateJsonCreator|Whether to generate a JSON creator constructor for required properties.| |true|
|generatePom|Whether to generate pom.xml if the file does not already exist.| |true|
|generateRootResources|Whether to generate the root resource and application classes, only useful if interfaceOnly is true.| |true|
|groupId|groupId in generated pom.xml| |org.openapitools|
Expand Down Expand Up @@ -75,6 +75,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|scmDeveloperConnection|SCM developer connection in generated pom.xml| |scm:git:git@github.com:openapitools/openapi-generator.git|
|scmUrl|SCM URL in generated pom.xml| |https://github.com/openapitools/openapi-generator|
|serializableModel|boolean - toggle &quot;implements Serializable&quot; for generated models| |false|
|serializationLibrary|Serialization library used for the generated models. 'jsonb' is only supported by the default library (&lt;default&gt;). With 'jsonb', dateLibrary defaults to 'java8' ('java8-localdatetime' is also supported, 'joda' and 'legacy' are not), openApiNullable is disabled and no type annotations are generated for discriminator/oneOf polymorphism. Models with additionalProperties are generated as subclasses of HashMap without a JSON-B specific mapping, so they are (de)serialized as plain maps: declared properties are not written and are read into map entries instead of their fields. Properties with a null value are omitted, except required nullable properties, which are written as null; JsonbConfig.withNullValues(true) does not change this because the generated @JsonbProperty annotations default to nillable = false. Depending on the JSON-B implementation, enum values inside maps (and, with older implementations, inside lists) may not use the generated enum serializers/deserializers.|<dl><dt>**jsonb**</dt><dd>Use JSON-B as serialization library</dd><dt>**jackson**</dt><dd>Use Jackson as serialization library</dd></dl>|jackson|
|serverPort|The port on which the server should be started| |8080|
|snapshotVersion|Uses a SNAPSHOT version.|<dl><dt>**true**</dt><dd>Use a SnapShot Version</dd><dt>**false**</dt><dd>Use a Release Version</dd></dl>|null|
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
Expand Down
6 changes: 6 additions & 0 deletions modules/openapi-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@
<version>2.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>3.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${swagger-parser-groupid.version}</groupId>
<artifactId>swagger-parser</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public JavaJAXRSCXFCDIServerCodegen() {
// jspecify support lives in the JavaJaxRS/spec templates; this generator uses its own
// cxf-cdi template directory, so the option would be advertised but have no effect.
removeOption(USE_JSPECIFY);
// JSON-B support lives in the JavaJaxRS/spec templates as well; cxf-cdi always uses Jackson.
removeOption(CodegenConstants.SERIALIZATION_LIBRARY);
}

@Override
Expand All @@ -82,6 +84,7 @@ public void processOpts() {
// then emit imports and package-info for templates this generator does not have.
additionalProperties.remove(USE_JSPECIFY);
setUseJspecify(false);
additionalProperties.remove(CodegenConstants.SERIALIZATION_LIBRARY);

super.processOpts();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationsMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.Collection;
Expand All @@ -49,6 +51,8 @@
*/
public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {

private final Logger LOGGER = LoggerFactory.getLogger(JavaJAXRSSpecServerCodegen.class);

public static final String RETURN_RESPONSE = "returnResponse";
public static final String RETURN_JBOSS_RESPONSE = "returnJBossResponse";
public static final String GENERATE_POM = "generatePom";
Expand All @@ -62,6 +66,8 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
public static final String USE_JAKARTA_SECURITY_ANNOTATIONS = "useJakartaSecurityAnnotations";
public static final String USE_ENUM_CASE_INSENSITIVE = "useEnumCaseInsensitive";
public static final String USE_SEALED = "useSealed";
public static final String SERIALIZATION_LIBRARY_JACKSON = "jackson";
public static final String SERIALIZATION_LIBRARY_JSONB = "jsonb";

public static final String QUARKUS_LIBRARY = "quarkus";
public static final String THORNTAIL_LIBRARY = "thorntail";
Expand All @@ -79,6 +85,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
private boolean useMicroProfileOpenAPIAnnotations = false;
private boolean useMutiny = false;
private boolean useJakartaSecurityAnnotations = false;
private String serializationLibrary = SERIALIZATION_LIBRARY_JACKSON;

@Setter
private boolean useEnumCaseInsensitive = false;
Expand Down Expand Up @@ -168,9 +175,16 @@ public JavaJAXRSSpecServerCodegen() {
cliOptions.add(CliOption.newBoolean(SUPPORT_ASYNC, "Wrap responses in CompletionStage type, allowing asynchronous computation (requires JAX-RS 2.1).", supportAsync));
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(GENERATE_JSON_CREATOR, "Whether to generate a JSON creator constructor for required properties.", generateJsonCreator));
cliOptions.add(CliOption.newBoolean(USE_ENUM_CASE_INSENSITIVE, "Use `equalsIgnoreCase` when String for enum comparison", useEnumCaseInsensitive));
cliOptions.add(CliOption.newBoolean(USE_SEALED, "Whether to generate sealed model interfaces and classes.", useSealed));
CliOption serializationLibraryOption = new CliOption(CodegenConstants.SERIALIZATION_LIBRARY, "Serialization library used for the generated models. 'jsonb' is only supported by the default library (" + DEFAULT_LIBRARY + "). With 'jsonb', dateLibrary defaults to 'java8' ('java8-localdatetime' is also supported, 'joda' and 'legacy' are not), openApiNullable is disabled and no type annotations are generated for discriminator/oneOf polymorphism. Models with additionalProperties are generated as subclasses of HashMap without a JSON-B specific mapping, so they are (de)serialized as plain maps: declared properties are not written and are read into map entries instead of their fields. Properties with a null value are omitted, except required nullable properties, which are written as null; JsonbConfig.withNullValues(true) does not change this because the generated @JsonbProperty annotations default to nillable = false. Depending on the JSON-B implementation, enum values inside maps (and, with older implementations, inside lists) may not use the generated enum serializers/deserializers.")
.defaultValue(SERIALIZATION_LIBRARY_JACKSON);
Map<String, String> serializationOptions = new HashMap<>();
serializationOptions.put(SERIALIZATION_LIBRARY_JACKSON, "Use Jackson as serialization library");
serializationOptions.put(SERIALIZATION_LIBRARY_JSONB, "Use JSON-B as serialization library");
serializationLibraryOption.setEnum(serializationOptions);
cliOptions.add(serializationLibraryOption);
cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use JSpecify for null checks: @NullMarked package-info and @Nullable on optional properties and parameters.", useJspecify));
}

Expand Down Expand Up @@ -233,6 +247,12 @@ public void processOpts() {
artifactId = "openapi-jaxrs-client";
}

convertPropertyToStringAndWriteBack(CodegenConstants.SERIALIZATION_LIBRARY, this::setSerializationLibrary);
// JSON-B cannot map the Joda types of the legacy default; select java8 before super.processOpts() applies the date type mappings
if (isJsonb() && !additionalProperties.containsKey(DATE_LIBRARY) && "legacy".equals(getDateLibrary())) {
setDateLibrary("java8");
}

super.processOpts();

// We need to call super.processOpts() before evaluating the `library`, otherwise `library` is null when set via `configOptions` instead of via `library.set("quarkus")` in Gradle
Expand All @@ -247,6 +267,10 @@ public void processOpts() {
+ "is incompatible with the javax.* namespace.");
}

if (isJsonb()) {
applyJsonbSerialization();
}

// expose flags to templates
additionalProperties.put(USE_SWAGGER_ANNOTATIONS, useSwaggerAnnotations);
additionalProperties.put(USE_SWAGGER_V3_ANNOTATIONS, useSwaggerV3Annotations);
Expand Down Expand Up @@ -342,6 +366,49 @@ public String getName() {
return "jaxrs-spec";
}

public String getSerializationLibrary() {
return serializationLibrary;
}

public void setSerializationLibrary(String serializationLibrary) {
if (SERIALIZATION_LIBRARY_JACKSON.equalsIgnoreCase(serializationLibrary)) {
this.serializationLibrary = SERIALIZATION_LIBRARY_JACKSON;
} else if (SERIALIZATION_LIBRARY_JSONB.equalsIgnoreCase(serializationLibrary)) {
this.serializationLibrary = SERIALIZATION_LIBRARY_JSONB;
} else {
throw new IllegalArgumentException("Unexpected " + CodegenConstants.SERIALIZATION_LIBRARY + " value: '"
+ serializationLibrary + "'. Supported values: " + SERIALIZATION_LIBRARY_JACKSON + ", " + SERIALIZATION_LIBRARY_JSONB);
}
}

public boolean isJsonb() {
return SERIALIZATION_LIBRARY_JSONB.equals(serializationLibrary);
}

private void applyJsonbSerialization() {
if (StringUtils.isNotEmpty(library) && !DEFAULT_LIBRARY.equals(library)) {
throw new IllegalArgumentException("'" + CodegenConstants.SERIALIZATION_LIBRARY + "=" + SERIALIZATION_LIBRARY_JSONB
+ "' is only supported by the library '" + DEFAULT_LIBRARY + "', not by '" + library + "'.");
}
if (!getDateLibrary().startsWith("java8")) {
throw new IllegalArgumentException("'" + DATE_LIBRARY + "=" + getDateLibrary() + "' is not supported with '"
+ CodegenConstants.SERIALIZATION_LIBRARY + "=" + SERIALIZATION_LIBRARY_JSONB + "'. Use 'java8' or 'java8-localdatetime'.");
}
if (additionalProperties.containsKey(JACKSON) && jackson) {
throw new IllegalArgumentException("Flags '" + JACKSON + "=true' and '" + CodegenConstants.SERIALIZATION_LIBRARY + "="
+ SERIALIZATION_LIBRARY_JSONB + "' are mutually exclusive. Please enable only one.");
}
if (additionalProperties.containsKey(OPENAPI_NULLABLE) && openApiNullable) {
LOGGER.warn("'{}' is not supported with '{}={}' and will be disabled.",
OPENAPI_NULLABLE, CodegenConstants.SERIALIZATION_LIBRARY, SERIALIZATION_LIBRARY_JSONB);
}
setJackson(false);
additionalProperties.put(JACKSON, false);
setOpenApiNullable(false);
additionalProperties.put(OPENAPI_NULLABLE, false);
additionalProperties.put(SERIALIZATION_LIBRARY_JSONB, true);
}

@Override
public CodegenModel fromModel(String name, Schema model) {
CodegenModel codegenModel = super.fromModel(name, model);
Expand All @@ -354,6 +421,10 @@ public CodegenModel fromModel(String name, Schema model) {
codegenModel.imports.remove("JsonProperty");
codegenModel.imports.remove("JsonTypeName");
}
if (isJsonb() && codegenModel.getAdditionalProperties() != null) {
LOGGER.warn("Model '{}' declares additionalProperties and is (de)serialized as a plain map with '{}={}'; declared properties are not mapped to its fields.",
codegenModel.name, CodegenConstants.SERIALIZATION_LIBRARY, SERIALIZATION_LIBRARY_JSONB);
}
if (useJspecify) {
codegenModel.imports.add("Nullable");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* @param value the value of the property
* @return self reference
*/
{{#jackson}}
@JsonAnySetter
{{/jackson}}
public {{classname}} putAdditionalProperty(String key, {{{datatypeWithEnum}}} value) {
this.put(key, value);
return this;
Expand All @@ -16,7 +18,9 @@
* Return the additional (undeclared) properties.
* @return the additional (undeclared) properties
*/
{{#jackson}}
@JsonAnyGetter
{{/jackson}}
public Map<String, {{{datatypeWithEnum}}}> getAdditionalProperties() {
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
@XmlType(name="{{datatypeWithEnum}}")
@XmlEnum({{dataType}}.class)
{{/withXml}}
{{>additionalEnumTypeAnnotations}}public enum {{datatypeWithEnum}} {
{{#jsonb}}@JsonbTypeSerializer({{datatypeWithEnum}}.Serializer.class)
@JsonbTypeDeserializer({{datatypeWithEnum}}.Deserializer.class)
{{/jsonb}}{{>additionalEnumTypeAnnotations}}public enum {{datatypeWithEnum}} {

{{#allowableValues}}
{{#enumVars}}{{#withXml}}@XmlEnumValue({{#lambda.javaStringLiteral}}{{{rawValue}}}{{/lambda.javaStringLiteral}}){{/withXml}}{{name}}({{dataType}}.valueOf({{{value}}})){{^-last}}, {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}
Expand Down Expand Up @@ -44,6 +46,7 @@

{{#jackson}}
@JsonCreator
{{/jackson}}
public static {{datatypeWithEnum}} fromValue({{dataType}} value) {
for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) {
if ({{#isString}}{{#useEnumCaseInsensitive}}b.value{{#dataFormat}}.toString(){{/dataFormat}}.equalsIgnoreCase({{#dataFormat}}value == null ? null : value.toString(){{/dataFormat}}{{^dataFormat}}value{{/dataFormat}}){{/useEnumCaseInsensitive}}{{^useEnumCaseInsensitive}}b.value.equals(value){{/useEnumCaseInsensitive}}{{/isString}}{{^isString}}b.value.equals(value){{/isString}}) {
Expand All @@ -52,5 +55,23 @@
}
{{#isNullable}}return null;{{/isNullable}}{{^isNullable}}{{#enumUnknownDefaultCase}}{{#allowableValues}}{{#enumVars}}{{#-last}}return {{{name}}};{{/-last}}{{/enumVars}}{{/allowableValues}}{{/enumUnknownDefaultCase}}{{^enumUnknownDefaultCase}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/enumUnknownDefaultCase}}{{/isNullable}}
}
{{/jackson}}
{{#jsonb}}

public static final class Serializer implements JsonbSerializer<{{datatypeWithEnum}}> {
@Override
public void serialize({{datatypeWithEnum}} obj, JsonGenerator generator, SerializationContext ctx) {
ctx.serialize(obj.value, generator);
}
}

public static final class Deserializer implements JsonbDeserializer<{{datatypeWithEnum}}> {
@Override
public {{datatypeWithEnum}} deserialize(JsonParser parser, DeserializationContext ctx, java.lang.reflect.Type rtType) {
if (parser.getValue().getValueType() == {{javaxPackage}}.json.JsonValue.ValueType.NULL) {
return null;
Comment thread
dean505 marked this conversation as resolved.
}
return fromValue(ctx.deserialize({{dataType}}.class, parser));
}
}
{{/jsonb}}
}
Loading