Skip to content

Commit f1bb82a

Browse files
committed
Adds @nullability annotation to Spring Boot generator
* issue-14427: [REQ][spring] Null-Safety annotations * issue-17382: [REQ] spring generator add Nullable annotations Motivations: * Have Spring Boot generator client properly annotated for nullability to be able to check code using them with tools like NullAway * As it is related to Spring then the `org.springframework.lang.Nullable` annotation was chosen to avoid discussion which `@Nullable` one is true one * `@NonNull` wasn't used as I didn't see much benefit of it. Anyhow, an empty constructor and/or setters allow to put a `null` value there Modifications: * Adds nullableAnnotation template to handle nullability annotation on vars * Adjust pojo templates to use the nullability template * Adapts tests Modifications: * Runs export_docs_generator.sh script to update samples
1 parent 289425b commit f1bb82a

File tree

6 files changed

+112
-20
lines changed

6 files changed

+112
-20
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ public void processOpts() {
446446
convertPropertyToStringAndWriteBack(RESOURCE_FOLDER, this::setResourceFolder);
447447

448448
typeMapping.put("file", "org.springframework.core.io.Resource");
449+
importMapping.put("Nullable", "org.springframework.lang.Nullable");
449450
importMapping.put("org.springframework.core.io.Resource", "org.springframework.core.io.Resource");
450451
importMapping.put("DateTimeFormat", "org.springframework.format.annotation.DateTimeFormat");
451452
importMapping.put("ApiIgnore", "springfox.documentation.annotations.ApiIgnore");
@@ -952,6 +953,8 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
952953
if (model.getVendorExtensions().containsKey("x-jackson-optional-nullable-helpers")) {
953954
model.imports.add("Arrays");
954955
}
956+
957+
model.imports.add("Nullable");
955958
}
956959

957960
@Override
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{^required}}{{^useOptional}}{{#openApiNullable}}{{^isNullable}}@Nullable {{/isNullable}}{{/openApiNullable}}{{^openApiNullable}}@Nullable {{/openApiNullable}}{{/useOptional}}{{/required}}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}
7272
{{#isContainer}}
7373
{{#useBeanValidation}}@Valid{{/useBeanValidation}}
7474
{{#openApiNullable}}
75-
private {{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}
75+
private {{>nullableAnnotation}}{{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}
7676
{{/openApiNullable}}
7777
{{^openApiNullable}}
78-
private {{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
78+
private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
7979
{{/openApiNullable}}
8080
{{/isContainer}}
8181
{{^isContainer}}
@@ -86,10 +86,10 @@ public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}
8686
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
8787
{{/isDateTime}}
8888
{{#openApiNullable}}
89-
private {{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#useOptional}} = Optional.{{^defaultValue}}empty(){{/defaultValue}}{{#defaultValue}}of({{{.}}}){{/defaultValue}};{{/useOptional}}{{^useOptional}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/useOptional}}{{/isNullable}}{{/required}}{{^isNullable}}{{#required}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/required}}{{/isNullable}}
89+
private {{>nullableAnnotation}}{{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#useOptional}} = Optional.{{^defaultValue}}empty(){{/defaultValue}}{{#defaultValue}}of({{{.}}}){{/defaultValue}};{{/useOptional}}{{^useOptional}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/useOptional}}{{/isNullable}}{{/required}}{{^isNullable}}{{#required}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/required}}{{/isNullable}}
9090
{{/openApiNullable}}
9191
{{^openApiNullable}}
92-
private {{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
92+
private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
9393
{{/openApiNullable}}
9494
{{/isContainer}}
9595
{{/vars}}
@@ -130,7 +130,7 @@ public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}
130130
/**
131131
* Constructor with all args parameters
132132
*/
133-
public {{classname}}({{#vendorExtensions.x-java-all-args-constructor-vars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-java-all-args-constructor-vars}}) {
133+
public {{classname}}({{#vendorExtensions.x-java-all-args-constructor-vars}}{{>nullableAnnotation}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-java-all-args-constructor-vars}}) {
134134
{{#parent}}
135135
super({{#parentVars}}{{name}}{{^-last}}, {{/-last}}{{/parentVars}});
136136
{{/parent}}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,18 @@ public PropertyAssert withType(final String expectedType) {
3030
public PropertyAnnotationsAssert assertPropertyAnnotations() {
3131
return new PropertyAnnotationsAssert(this, actual.getAnnotations());
3232
}
33+
34+
public PropertyAnnotationsAssert doesNotHaveAnnotation(String annotationName) {
35+
return new PropertyAnnotationsAssert(
36+
this,
37+
actual.getAnnotations()
38+
).doesNotContainWithName(annotationName);
39+
}
40+
41+
public PropertyAnnotationsAssert hasAnnotation(String annotationName) {
42+
return new PropertyAnnotationsAssert(
43+
this,
44+
actual.getAnnotations()
45+
).containsWithName(annotationName);
46+
}
3347
}

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

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4859,7 +4859,7 @@ public void optionalListShouldBeEmpty() throws IOException {
48594859
.collect(Collectors.toMap(File::getName, Function.identity()));
48604860

48614861
JavaFileAssert.assertThat(files.get("PetDto.java"))
4862-
.fileContains("private List<@Valid TagDto> tags = new ArrayList<>();")
4862+
.fileContains("private @Nullable List<@Valid TagDto> tags = new ArrayList<>();")
48634863
.fileContains("private List<String> photoUrls = new ArrayList<>();");
48644864

48654865
}
@@ -4893,20 +4893,20 @@ public void testCollectionTypesWithDefaults_issue_18102() throws IOException {
48934893
.collect(Collectors.toMap(File::getName, Function.identity()));
48944894

48954895
JavaFileAssert.assertThat(files.get("PetDto.java"))
4896-
.fileContains("private List<@Valid TagDto> tags")
4897-
.fileContains("private List<@Valid TagDto> tagsDefaultList = new ArrayList<>()")
4898-
.fileContains("private Set<@Valid TagDto> tagsUnique")
4899-
.fileContains("private Set<@Valid TagDto> tagsDefaultSet = new LinkedHashSet<>();")
4900-
.fileContains("private List<String> stringList")
4901-
.fileContains("private List<String> stringDefaultList = new ArrayList<>(Arrays.asList(\"A\", \"B\"));")
4902-
.fileContains("private List<String> stringEmptyDefaultList = new ArrayList<>();")
4903-
.fileContains("Set<String> stringSet")
4904-
.fileContains("private Set<String> stringDefaultSet = new LinkedHashSet<>(Arrays.asList(\"A\", \"B\"));")
4905-
.fileContains("private Set<String> stringEmptyDefaultSet = new LinkedHashSet<>();")
4906-
.fileDoesNotContain("private List<@Valid TagDto> tags = new ArrayList<>()")
4907-
.fileDoesNotContain("private Set<@Valid TagDto> tagsUnique = new LinkedHashSet<>()")
4908-
.fileDoesNotContain("private List<String> stringList = new ArrayList<>()")
4909-
.fileDoesNotContain("private Set<String> stringSet = new LinkedHashSet<>()");
4896+
.fileContains("private @Nullable List<@Valid TagDto> tags")
4897+
.fileContains("private @Nullable List<@Valid TagDto> tagsDefaultList = new ArrayList<>()")
4898+
.fileContains("private @Nullable Set<@Valid TagDto> tagsUnique")
4899+
.fileContains("private @Nullable Set<@Valid TagDto> tagsDefaultSet = new LinkedHashSet<>();")
4900+
.fileContains("private @Nullable List<String> stringList")
4901+
.fileContains("private @Nullable List<String> stringDefaultList = new ArrayList<>(Arrays.asList(\"A\", \"B\"));")
4902+
.fileContains("private @Nullable List<String> stringEmptyDefaultList = new ArrayList<>();")
4903+
.fileContains("@Nullable Set<String> stringSet")
4904+
.fileContains("private @Nullable Set<String> stringDefaultSet = new LinkedHashSet<>(Arrays.asList(\"A\", \"B\"));")
4905+
.fileContains("private @Nullable Set<String> stringEmptyDefaultSet = new LinkedHashSet<>();")
4906+
.fileDoesNotContain("List<@Valid TagDto> tags = new ArrayList<>()")
4907+
.fileDoesNotContain("Set<@Valid TagDto> tagsUnique = new LinkedHashSet<>()")
4908+
.fileDoesNotContain("List<String> stringList = new ArrayList<>()")
4909+
.fileDoesNotContain("Set<String> stringSet = new LinkedHashSet<>()");
49104910
}
49114911

49124912
@Test
@@ -5099,4 +5099,63 @@ public void testEnumUnknownDefaultCaseDeserializationNotSet_issue13241() throws
50995099
.assertMethod("build")
51005100
.doesNotHaveAnnotation("Deprecated");
51015101
}
5102+
5103+
@Test
5104+
public void shouldAnnotateNonRequiredFieldsAsNullable() throws IOException {
5105+
SpringCodegen codegen = new SpringCodegen();
5106+
codegen.setLibrary(SPRING_BOOT);
5107+
5108+
Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
5109+
var file = files.get("Item.java");
5110+
5111+
JavaFileAssert.assertThat(file)
5112+
.assertProperty("mandatoryName")
5113+
.doesNotHaveAnnotation("Nullable");
5114+
JavaFileAssert.assertThat(file)
5115+
.assertProperty("optionalDescription")
5116+
.hasAnnotation("Nullable");
5117+
JavaFileAssert.assertThat(file)
5118+
.assertProperty("nullableStr")
5119+
.doesNotHaveAnnotation("Nullable");
5120+
}
5121+
5122+
@Test
5123+
public void shouldNotAnnotateNonRequiredFieldsAsNullableWhileUseOptional() throws IOException {
5124+
SpringCodegen codegen = new SpringCodegen();
5125+
codegen.setLibrary(SPRING_BOOT);
5126+
codegen.setUseOptional(true);
5127+
5128+
Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
5129+
var file = files.get("Item.java");
5130+
5131+
JavaFileAssert.assertThat(file)
5132+
.assertProperty("mandatoryName")
5133+
.doesNotHaveAnnotation("Nullable");
5134+
JavaFileAssert.assertThat(file)
5135+
.assertProperty("optionalDescription")
5136+
.doesNotHaveAnnotation("Nullable");
5137+
JavaFileAssert.assertThat(file)
5138+
.assertProperty("nullableStr")
5139+
.doesNotHaveAnnotation("Nullable");
5140+
}
5141+
5142+
@Test
5143+
public void shouldNotAnnotateNonRequiredFieldsAsNullableWhileNotUsingOpenApiNullable() throws IOException {
5144+
SpringCodegen codegen = new SpringCodegen();
5145+
codegen.setLibrary(SPRING_BOOT);
5146+
codegen.setOpenApiNullable(false);
5147+
5148+
Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
5149+
var file = files.get("Item.java");
5150+
5151+
JavaFileAssert.assertThat(file)
5152+
.assertProperty("mandatoryName")
5153+
.doesNotHaveAnnotation("Nullable");
5154+
JavaFileAssert.assertThat(file)
5155+
.assertProperty("optionalDescription")
5156+
.hasAnnotation("Nullable");
5157+
JavaFileAssert.assertThat(file)
5158+
.assertProperty("nullableStr")
5159+
.hasAnnotation("Nullable");
5160+
}
51025161
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
openapi: 3.0.0
2+
components:
3+
schemas:
4+
Item:
5+
type: object
6+
required:
7+
- mandatoryName
8+
properties:
9+
mandatoryName:
10+
type: String
11+
optionalDescription:
12+
type: string
13+
nullableStr:
14+
type: String
15+
nullable: true

0 commit comments

Comments
 (0)