Skip to content

Commit bde43b4

Browse files
committed
feat: add optional getters for nullable fields
1 parent 1dc89bd commit bde43b4

8 files changed

Lines changed: 139 additions & 5 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
119119
public static final String DEFAULT_TEST_FOLDER = "${project.build.directory}/generated-test-sources/openapi";
120120
public static final String GENERATE_CONSTRUCTOR_WITH_ALL_ARGS = "generateConstructorWithAllArgs";
121121
public static final String GENERATE_BUILDERS = "generateBuilders";
122+
public static final String OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS_ONLY = "optionalGettersForNullableFieldsOnly";
122123

123124
@Getter @Setter
124125
protected String dateLibrary = "java8";
@@ -182,6 +183,8 @@ protected enum ENUM_PROPERTY_NAMING_TYPE {MACRO_CASE, legacy, original}
182183
@Getter @Setter
183184
protected String booleanGetterPrefix = "get";
184185
@Setter protected boolean ignoreAnyOfInEnum = false;
186+
@Getter @Setter
187+
protected boolean optionalGettersForNullableFieldsOnly = false;
185188
@Setter protected String parentGroupId = "";
186189
@Setter protected String parentArtifactId = "";
187190
@Setter protected String parentVersion = "";
@@ -370,6 +373,7 @@ public AbstractJavaCodegen() {
370373
cliOptions.add(CliOption.newBoolean(CONTAINER_DEFAULT_TO_NULL, "Set containers (array, set, map) default to null"));
371374
cliOptions.add(CliOption.newBoolean(GENERATE_CONSTRUCTOR_WITH_ALL_ARGS, "whether to generate a constructor for all arguments").defaultValue(Boolean.FALSE.toString()));
372375
cliOptions.add(CliOption.newBoolean(GENERATE_BUILDERS, "Whether to generate builders for models").defaultValue(Boolean.FALSE.toString()));
376+
cliOptions.add(CliOption.newBoolean(OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS_ONLY, "Make getters of nullable / non-required fields return Optional<T> while keeping the field and setter as the raw type. Opt-in, disabled by default.", optionalGettersForNullableFieldsOnly));
373377
cliOptions.add(CliOption.newBoolean(DISABLE_DISCRIMINATOR_JSON_IGNORE_PROPERTIES, "Ignore discriminator field type for Jackson serialization", disableDiscriminatorJsonIgnoreProperties));
374378

375379
cliOptions.add(CliOption.newString(CodegenConstants.PARENT_GROUP_ID, CodegenConstants.PARENT_GROUP_ID_DESC));
@@ -452,6 +456,7 @@ public void processOpts() {
452456

453457
convertPropertyToBooleanAndWriteBack(GENERATE_CONSTRUCTOR_WITH_ALL_ARGS, this::setGenerateConstructorWithAllArgs);
454458
convertPropertyToBooleanAndWriteBack(GENERATE_BUILDERS, this::setGenerateBuilders);
459+
convertPropertyToBooleanAndWriteBack(OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS_ONLY, this::setOptionalGettersForNullableFieldsOnly);
455460
convertPropertyToBooleanAndWriteBack(DISABLE_DISCRIMINATOR_JSON_IGNORE_PROPERTIES, this::setDisableDiscriminatorJsonIgnoreProperties);
456461
if (StringUtils.isEmpty(System.getenv("JAVA_POST_PROCESS_FILE"))) {
457462
LOGGER.info("Environment variable JAVA_POST_PROCESS_FILE not defined so the Java code may not be properly formatted. To define it, try 'export JAVA_POST_PROCESS_FILE=\"/usr/local/bin/clang-format -i\"' (Linux/Mac)");

modules/openapi-generator/src/main/resources/Java/libraries/restclient/pojo.mustache

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#v
240240
@JsonIgnore
241241
{{/vendorExtensions.x-is-jackson-optional-nullable}}
242242
{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}}
243-
public {{>nullableDatatypeWithEnum}} {{getter}}() {
243+
public {{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^vendorExtensions.x-is-jackson-optional-nullable}}java.util.Optional<{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}}{{>nullableDatatypeWithEnum}}{{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^vendorExtensions.x-is-jackson-optional-nullable}}>{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}} {{getter}}() {
244244
{{#vendorExtensions.x-is-jackson-optional-nullable}}
245245
{{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}}
246246
if ({{name}} == null) {
@@ -250,7 +250,14 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#v
250250
return {{name}}.orElse(null);
251251
{{/vendorExtensions.x-is-jackson-optional-nullable}}
252252
{{^vendorExtensions.x-is-jackson-optional-nullable}}
253+
{{#optionalGettersForNullableFieldsOnly}}{{^required}}
254+
return java.util.Optional.ofNullable({{name}});
255+
{{/required}}{{#required}}
253256
return {{name}};
257+
{{/required}}{{/optionalGettersForNullableFieldsOnly}}
258+
{{^optionalGettersForNullableFieldsOnly}}
259+
return {{name}};
260+
{{/optionalGettersForNullableFieldsOnly}}
254261
{{/vendorExtensions.x-is-jackson-optional-nullable}}
255262
}
256263

modules/openapi-generator/src/main/resources/Java/libraries/resttemplate/pojo.mustache

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
240240
@JsonIgnore
241241
{{/vendorExtensions.x-is-jackson-optional-nullable}}
242242
{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}}
243-
public {{>nullableDatatypeWithEnum}} {{getter}}() {
243+
public {{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^vendorExtensions.x-is-jackson-optional-nullable}}java.util.Optional<{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}}{{>nullableDatatypeWithEnum}}{{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^vendorExtensions.x-is-jackson-optional-nullable}}>{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}} {{getter}}() {
244244
{{#vendorExtensions.x-is-jackson-optional-nullable}}
245245
{{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}}
246246
if ({{name}} == null) {
@@ -250,7 +250,14 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
250250
return {{name}}.orElse(null);
251251
{{/vendorExtensions.x-is-jackson-optional-nullable}}
252252
{{^vendorExtensions.x-is-jackson-optional-nullable}}
253+
{{#optionalGettersForNullableFieldsOnly}}{{^required}}
254+
return java.util.Optional.ofNullable({{name}});
255+
{{/required}}{{#required}}
253256
return {{name}};
257+
{{/required}}{{/optionalGettersForNullableFieldsOnly}}
258+
{{^optionalGettersForNullableFieldsOnly}}
259+
return {{name}};
260+
{{/optionalGettersForNullableFieldsOnly}}
254261
{{/vendorExtensions.x-is-jackson-optional-nullable}}
255262
}
256263

modules/openapi-generator/src/main/resources/Java/libraries/webclient/pojo.mustache

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#v
240240
@JsonIgnore
241241
{{/vendorExtensions.x-is-jackson-optional-nullable}}
242242
{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}}
243-
public {{>nullableDatatypeWithEnum}} {{getter}}() {
243+
public {{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^vendorExtensions.x-is-jackson-optional-nullable}}java.util.Optional<{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}}{{>nullableDatatypeWithEnum}}{{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^vendorExtensions.x-is-jackson-optional-nullable}}>{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}} {{getter}}() {
244244
{{#vendorExtensions.x-is-jackson-optional-nullable}}
245245
{{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}}
246246
if ({{name}} == null) {
@@ -250,7 +250,14 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#v
250250
return {{name}}.orElse(null);
251251
{{/vendorExtensions.x-is-jackson-optional-nullable}}
252252
{{^vendorExtensions.x-is-jackson-optional-nullable}}
253+
{{#optionalGettersForNullableFieldsOnly}}{{^required}}
254+
return java.util.Optional.ofNullable({{name}});
255+
{{/required}}{{#required}}
253256
return {{name}};
257+
{{/required}}{{/optionalGettersForNullableFieldsOnly}}
258+
{{^optionalGettersForNullableFieldsOnly}}
259+
return {{name}};
260+
{{/optionalGettersForNullableFieldsOnly}}
254261
{{/vendorExtensions.x-is-jackson-optional-nullable}}
255262
}
256263

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
243243
@JsonIgnore
244244
{{/vendorExtensions.x-is-jackson-optional-nullable}}
245245
{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}}
246-
public {{{datatypeWithEnum}}} {{getter}}() {
246+
public {{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^vendorExtensions.x-is-jackson-optional-nullable}}java.util.Optional<{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}}{{{datatypeWithEnum}}}{{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^vendorExtensions.x-is-jackson-optional-nullable}}>{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}} {{getter}}() {
247247
{{#vendorExtensions.x-is-jackson-optional-nullable}}
248248
{{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}}
249249
if ({{name}} == null) {
@@ -253,7 +253,14 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
253253
return {{name}}.orElse(null);
254254
{{/vendorExtensions.x-is-jackson-optional-nullable}}
255255
{{^vendorExtensions.x-is-jackson-optional-nullable}}
256+
{{#optionalGettersForNullableFieldsOnly}}{{^required}}
257+
return java.util.Optional.ofNullable({{name}});
258+
{{/required}}{{#required}}
256259
return {{name}};
260+
{{/required}}{{/optionalGettersForNullableFieldsOnly}}
261+
{{^optionalGettersForNullableFieldsOnly}}
262+
return {{name}};
263+
{{/optionalGettersForNullableFieldsOnly}}
257264
{{/vendorExtensions.x-is-jackson-optional-nullable}}
258265
}
259266

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,17 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
228228
{{#deprecated}}
229229
@Deprecated
230230
{{/deprecated}}
231-
{{#jackson}}{{>jackson_annotations}}{{/jackson}}{{#withXml}}{{>xmlAccessorAnnotation}}{{/withXml}} public {{>nullableAnnotation}}{{>nullableDataTypeBeanValidation}} {{getter}}() {
231+
{{#jackson}}{{>jackson_annotations}}{{/jackson}}{{#withXml}}{{>xmlAccessorAnnotation}}{{/withXml}} public {{>nullableAnnotation}}{{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^isNullable}}java.util.Optional<{{/isNullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}}{{>nullableDataTypeBeanValidation}}{{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^isNullable}}>{{/isNullable}}{{/required}}{{/optionalGettersForNullableFieldsOnly}} {{getter}}() {
232+
{{#optionalGettersForNullableFieldsOnly}}{{^required}}{{^isNullable}}
233+
return java.util.Optional.ofNullable({{name}});
234+
{{/isNullable}}{{#isNullable}}
232235
return {{name}};
236+
{{/isNullable}}{{/required}}{{#required}}
237+
return {{name}};
238+
{{/required}}{{/optionalGettersForNullableFieldsOnly}}
239+
{{^optionalGettersForNullableFieldsOnly}}
240+
return {{name}};
241+
{{/optionalGettersForNullableFieldsOnly}}
233242
}
234243
{{/lombok.Getter}}
235244

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4536,5 +4536,48 @@ public void testUseDeductionForOneInterfaces() {
45364536

45374537
}
45384538

4539+
@Test
4540+
public void testOptionalGettersForNullableFieldsOnly() {
4541+
final Path output = newTempFolder();
4542+
final CodegenConfigurator configurator = new CodegenConfigurator()
4543+
.setGeneratorName(JAVA_GENERATOR)
4544+
.setLibrary(JavaClientCodegen.RESTCLIENT)
4545+
.addAdditionalProperty(AbstractJavaCodegen.OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS_ONLY, true)
4546+
.setInputSpec("src/test/resources/3_0/java/builder.yaml")
4547+
.setOutputDir(output.toString().replace("\\", "/"));
4548+
4549+
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
4550+
4551+
// Generated sources must compile (validates Optional getter + raw field/setter are consistent).
4552+
validateJavaSourceFiles(files);
4553+
4554+
assertThat(output.resolve("src/main/java/org/openapitools/client/model/SimpleObject.java")).content().contains(
4555+
"public java.util.Optional<String> getSimple() {",
4556+
"return java.util.Optional.ofNullable(simple);",
4557+
"private String simple;",
4558+
"public void setSimple(@jakarta.annotation.Nullable String simple) {"
4559+
);
4560+
// Nullable fields keep JsonNullable and must not be wrapped in Optional.
4561+
assertThat(output.resolve("src/main/java/org/openapitools/client/model/SimpleObject.java")).content()
4562+
.doesNotContain("public java.util.Optional<String> getNullableObject() {");
4563+
}
4564+
4565+
@Test
4566+
public void testOptionalGettersForNullableFieldsOnlyDisabledByDefault() {
4567+
final Path output = newTempFolder();
4568+
final CodegenConfigurator configurator = new CodegenConfigurator()
4569+
.setGeneratorName(JAVA_GENERATOR)
4570+
.setLibrary(JavaClientCodegen.RESTCLIENT)
4571+
.setInputSpec("src/test/resources/3_0/java/builder.yaml")
4572+
.setOutputDir(output.toString().replace("\\", "/"));
4573+
4574+
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
4575+
4576+
validateJavaSourceFiles(files);
4577+
4578+
// Without the opt-in flag the getter keeps returning the raw type (backward compatible).
4579+
assertThat(output.resolve("src/main/java/org/openapitools/client/model/SimpleObject.java")).content()
4580+
.doesNotContain("return java.util.Optional.ofNullable(simple);");
4581+
}
45394582

45404583
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5372,6 +5372,55 @@ void testBuilderJavaSpring_useOptional() throws IOException {
53725372
"SimpleObject.Builder nb(BigDecimal nb) {");
53735373
}
53745374

5375+
@Test
5376+
void testOptionalGettersForNullableFieldsOnly() throws IOException {
5377+
Map<String, File> files = generateFromContract(
5378+
"src/test/resources/3_0/java/builder.yaml",
5379+
SPRING_BOOT,
5380+
Map.of(
5381+
SpringCodegen.OPENAPI_NULLABLE, true,
5382+
AbstractJavaCodegen.OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS_ONLY, true,
5383+
INTERFACE_ONLY, "true"
5384+
)
5385+
);
5386+
5387+
// Non-required, non-nullable fields: getter returns Optional, field and setter stay raw.
5388+
JavaFileAssert.assertThat(files.get("SimpleObject.java"))
5389+
.fileContains(
5390+
"public @Nullable java.util.Optional<String> getSimple() {",
5391+
"return java.util.Optional.ofNullable(simple);",
5392+
"private @Nullable String simple;",
5393+
"public void setSimple(@Nullable String simple) {")
5394+
// The backing field and setter must remain the raw type, never Optional.
5395+
.fileDoesNotContain(
5396+
"private @Nullable java.util.Optional<String> simple;",
5397+
"public void setSimple(java.util.Optional<String> simple)");
5398+
5399+
// Nullable fields keep JsonNullable semantics and must NOT be wrapped in Optional.
5400+
JavaFileAssert.assertThat(files.get("SimpleObject.java"))
5401+
.fileContains("public JsonNullable<String> getNullableObject() {")
5402+
.fileDoesNotContain("public java.util.Optional<String> getNullableObject() {");
5403+
}
5404+
5405+
@Test
5406+
void testOptionalGettersForNullableFieldsOnlyDisabledByDefault() throws IOException {
5407+
Map<String, File> files = generateFromContract(
5408+
"src/test/resources/3_0/java/builder.yaml",
5409+
SPRING_BOOT,
5410+
Map.of(
5411+
SpringCodegen.OPENAPI_NULLABLE, true,
5412+
INTERFACE_ONLY, "true"
5413+
)
5414+
);
5415+
5416+
// Without the opt-in flag the getter must keep returning the raw type (backward compatible).
5417+
JavaFileAssert.assertThat(files.get("SimpleObject.java"))
5418+
.fileContains("getSimple()")
5419+
.fileDoesNotContain(
5420+
"public java.util.Optional<String> getSimple() {",
5421+
"return java.util.Optional.ofNullable(simple);");
5422+
}
5423+
53755424
@Test
53765425
public void optionalListShouldBeEmpty() throws IOException {
53775426
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();

0 commit comments

Comments
 (0)