Skip to content

Commit cba756f

Browse files
authored
Adds @nullable annotation to Spring Boot generator (#20345)
* Adds @nullable 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 * samples update * excludes Spring @nullable from java-camel * ones with defaults shouldn't be annotated as @nullable * updates samples * adds AllArgConstructor generation tests * adds container tests
1 parent 4b5dfc4 commit cba756f

File tree

1,181 files changed

+4056
-2671
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,181 files changed

+4056
-2671
lines changed

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

Lines changed: 6 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,11 @@ 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+
// to prevent inheritors (JavaCamelServerCodegen etc.) mistakenly use it
958+
if (getName().contains("spring")) {
959+
model.imports.add("Nullable");
960+
}
955961
}
956962

957963
@Override
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{^required}}{{^defaultValue}}{{^useOptional}}{{#openApiNullable}}{{^isNullable}}@Nullable {{/isNullable}}{{/openApiNullable}}{{^openApiNullable}}@Nullable {{/openApiNullable}}{{/useOptional}}{{/defaultValue}}{{#defaultValue}}{{^openApiNullable}}{{#isNullable}}@Nullable {{/isNullable}}{{/openApiNullable}}{{/defaultValue}}{{/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: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4893,14 +4893,14 @@ 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")
4896+
.fileContains("private @Nullable List<@Valid TagDto> tags")
48974897
.fileContains("private List<@Valid TagDto> tagsDefaultList = new ArrayList<>()")
4898-
.fileContains("private Set<@Valid TagDto> tagsUnique")
4898+
.fileContains("private @Nullable Set<@Valid TagDto> tagsUnique")
48994899
.fileContains("private Set<@Valid TagDto> tagsDefaultSet = new LinkedHashSet<>();")
4900-
.fileContains("private List<String> stringList")
4900+
.fileContains("private @Nullable List<String> stringList")
49014901
.fileContains("private List<String> stringDefaultList = new ArrayList<>(Arrays.asList(\"A\", \"B\"));")
49024902
.fileContains("private List<String> stringEmptyDefaultList = new ArrayList<>();")
4903-
.fileContains("Set<String> stringSet")
4903+
.fileContains("@Nullable Set<String> stringSet")
49044904
.fileContains("private Set<String> stringDefaultSet = new LinkedHashSet<>(Arrays.asList(\"A\", \"B\"));")
49054905
.fileContains("private Set<String> stringEmptyDefaultSet = new LinkedHashSet<>();")
49064906
.fileDoesNotContain("private List<@Valid TagDto> tags = new ArrayList<>()")
@@ -5099,4 +5099,156 @@ 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+
codegen.setGenerateConstructorWithAllArgs(true);
5108+
5109+
Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
5110+
var file = files.get("Item.java");
5111+
5112+
JavaFileAssert.assertThat(file)
5113+
.assertProperty("mandatoryName")
5114+
.doesNotHaveAnnotation("Nullable");
5115+
JavaFileAssert.assertThat(file)
5116+
.assertProperty("optionalDescription")
5117+
.hasAnnotation("Nullable");
5118+
JavaFileAssert.assertThat(file)
5119+
.assertProperty("optionalOneWithDefault")
5120+
.doesNotHaveAnnotation("Nullable");
5121+
JavaFileAssert.assertThat(file)
5122+
.assertProperty("nullableStr")
5123+
.doesNotHaveAnnotation("Nullable");
5124+
JavaFileAssert.assertThat(file)
5125+
.assertProperty("mandatoryContainer")
5126+
.doesNotHaveAnnotation("Nullable");
5127+
JavaFileAssert.assertThat(file)
5128+
.assertProperty("optionalContainer")
5129+
.doesNotHaveAnnotation("Nullable");
5130+
JavaFileAssert.assertThat(file)
5131+
.assertProperty("optionalContainerWithDefault")
5132+
.doesNotHaveAnnotation("Nullable");
5133+
JavaFileAssert.assertThat(file)
5134+
.assertProperty("nullableContainer")
5135+
.doesNotHaveAnnotation("Nullable");
5136+
JavaFileAssert.assertThat(file)
5137+
.fileContains(
5138+
"public Item(" +
5139+
"String mandatoryName," +
5140+
" @Nullable String optionalDescription," +
5141+
" String optionalOneWithDefault," +
5142+
" String nullableStr," +
5143+
" List<String> mandatoryContainer," +
5144+
" List<String> optionalContainer," +
5145+
" List<String> optionalContainerWithDefault," +
5146+
" List<String> nullableContainer)"
5147+
);
5148+
}
5149+
5150+
@Test
5151+
public void shouldAnnotateNonRequiredFieldsAsNullableWhenSetContainerDefaultToNull() throws IOException {
5152+
SpringCodegen codegen = new SpringCodegen();
5153+
codegen.setLibrary(SPRING_BOOT);
5154+
codegen.setGenerateConstructorWithAllArgs(true);
5155+
codegen.setContainerDefaultToNull(true);
5156+
5157+
Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
5158+
var file = files.get("Item.java");
5159+
5160+
JavaFileAssert.assertThat(file)
5161+
.assertProperty("mandatoryContainer")
5162+
.doesNotHaveAnnotation("Nullable");
5163+
JavaFileAssert.assertThat(file)
5164+
.assertProperty("optionalContainer")
5165+
.hasAnnotation("Nullable");
5166+
JavaFileAssert.assertThat(file)
5167+
.assertProperty("optionalContainerWithDefault")
5168+
.doesNotHaveAnnotation("Nullable");
5169+
JavaFileAssert.assertThat(file)
5170+
.assertProperty("nullableContainer")
5171+
.doesNotHaveAnnotation("Nullable");
5172+
JavaFileAssert.assertThat(file)
5173+
.fileContains(
5174+
", List<String> mandatoryContainer," +
5175+
" @Nullable List<String> optionalContainer," +
5176+
" List<String> optionalContainerWithDefault," +
5177+
" List<String> nullableContainer)"
5178+
);
5179+
}
5180+
5181+
@Test
5182+
public void shouldNotAnnotateNonRequiredFieldsAsNullableWhileUseOptional() throws IOException {
5183+
SpringCodegen codegen = new SpringCodegen();
5184+
codegen.setLibrary(SPRING_BOOT);
5185+
codegen.setGenerateConstructorWithAllArgs(true);
5186+
codegen.setUseOptional(true);
5187+
5188+
Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
5189+
var file = files.get("Item.java");
5190+
5191+
JavaFileAssert.assertThat(file)
5192+
.assertProperty("mandatoryName")
5193+
.doesNotHaveAnnotation("Nullable");
5194+
JavaFileAssert.assertThat(file)
5195+
.assertProperty("optionalDescription")
5196+
.doesNotHaveAnnotation("Nullable");
5197+
JavaFileAssert.assertThat(file)
5198+
.assertProperty("optionalOneWithDefault")
5199+
.doesNotHaveAnnotation("Nullable");
5200+
JavaFileAssert.assertThat(file)
5201+
.assertProperty("nullableStr")
5202+
.doesNotHaveAnnotation("Nullable");
5203+
JavaFileAssert.assertThat(file)
5204+
.fileContains(
5205+
"public Item(String mandatoryName, String optionalDescription," +
5206+
" String optionalOneWithDefault, String nullableStr"
5207+
);
5208+
}
5209+
5210+
@Test
5211+
public void shouldAnnotateNonRequiredFieldsAsNullableWhileNotUsingOpenApiNullableAndContainerDefaultToNullSet() throws IOException {
5212+
SpringCodegen codegen = new SpringCodegen();
5213+
codegen.setLibrary(SPRING_BOOT);
5214+
codegen.setGenerateConstructorWithAllArgs(true);
5215+
codegen.setOpenApiNullable(false);
5216+
codegen.setContainerDefaultToNull(true);
5217+
5218+
Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
5219+
var file = files.get("Item.java");
5220+
5221+
JavaFileAssert.assertThat(file)
5222+
.assertProperty("mandatoryName")
5223+
.doesNotHaveAnnotation("Nullable");
5224+
JavaFileAssert.assertThat(file)
5225+
.assertProperty("optionalDescription")
5226+
.hasAnnotation("Nullable");
5227+
JavaFileAssert.assertThat(file)
5228+
.assertProperty("optionalOneWithDefault")
5229+
.doesNotHaveAnnotation("Nullable");
5230+
JavaFileAssert.assertThat(file)
5231+
.assertProperty("nullableStr")
5232+
.hasAnnotation("Nullable");
5233+
JavaFileAssert.assertThat(file)
5234+
.assertProperty("mandatoryContainer")
5235+
.doesNotHaveAnnotation("Nullable");
5236+
JavaFileAssert.assertThat(file)
5237+
.assertProperty("optionalContainer")
5238+
.hasAnnotation("Nullable");
5239+
JavaFileAssert.assertThat(file)
5240+
.assertProperty("optionalContainerWithDefault")
5241+
.doesNotHaveAnnotation("Nullable");
5242+
JavaFileAssert.assertThat(file)
5243+
.assertProperty("nullableContainer")
5244+
.hasAnnotation("Nullable");
5245+
5246+
JavaFileAssert.assertThat(file)
5247+
.fileContains(
5248+
" List<String> mandatoryContainer," +
5249+
" @Nullable List<String> optionalContainer," +
5250+
" List<String> optionalContainerWithDefault," +
5251+
" @Nullable List<String> nullableContainer)"
5252+
);
5253+
}
51025254
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
openapi: 3.0.0
2+
components:
3+
schemas:
4+
Item:
5+
type: object
6+
required:
7+
- mandatoryName
8+
- mandatoryContainer
9+
properties:
10+
mandatoryName:
11+
type: string
12+
optionalDescription:
13+
type: string
14+
optionalOneWithDefault:
15+
type: string
16+
default: "someDefaultValue"
17+
nullableStr:
18+
type: string
19+
nullable: true
20+
mandatoryContainer:
21+
type: array
22+
items:
23+
type: string
24+
optionalContainer:
25+
type: array
26+
items:
27+
type: string
28+
optionalContainerWithDefault:
29+
type: array
30+
items:
31+
type: string
32+
default: [ ]
33+
nullableContainer:
34+
type: array
35+
items:
36+
type: string
37+
nullable: true

samples/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.time.LocalDate;
1111
import java.time.OffsetDateTime;
1212
import org.springframework.format.annotation.DateTimeFormat;
13+
import org.springframework.lang.Nullable;
1314
import org.openapitools.jackson.nullable.JsonNullable;
1415
import java.time.OffsetDateTime;
1516
import javax.validation.Valid;

samples/client/petstore/spring-cloud-deprecated/src/main/java/org/openapitools/model/Category.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.Objects;
55
import com.fasterxml.jackson.annotation.JsonProperty;
66
import com.fasterxml.jackson.annotation.JsonCreator;
7+
import org.springframework.lang.Nullable;
78
import org.openapitools.jackson.nullable.JsonNullable;
89
import java.time.OffsetDateTime;
910
import javax.validation.Valid;
@@ -22,9 +23,9 @@
2223
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
2324
public class Category {
2425

25-
private Long id;
26+
private @Nullable Long id;
2627

27-
private String name;
28+
private @Nullable String name;
2829

2930
public Category id(Long id) {
3031
this.id = id;

samples/client/petstore/spring-cloud-deprecated/src/main/java/org/openapitools/model/ModelApiResponse.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.annotation.JsonProperty;
66
import com.fasterxml.jackson.annotation.JsonCreator;
77
import com.fasterxml.jackson.annotation.JsonTypeName;
8+
import org.springframework.lang.Nullable;
89
import org.openapitools.jackson.nullable.JsonNullable;
910
import java.time.OffsetDateTime;
1011
import javax.validation.Valid;
@@ -24,11 +25,11 @@
2425
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
2526
public class ModelApiResponse {
2627

27-
private Integer code;
28+
private @Nullable Integer code;
2829

29-
private String type;
30+
private @Nullable String type;
3031

31-
private String message;
32+
private @Nullable String message;
3233

3334
public ModelApiResponse code(Integer code) {
3435
this.code = code;

samples/client/petstore/spring-cloud-deprecated/src/main/java/org/openapitools/model/Order.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.fasterxml.jackson.annotation.JsonValue;
88
import java.time.OffsetDateTime;
99
import org.springframework.format.annotation.DateTimeFormat;
10+
import org.springframework.lang.Nullable;
1011
import org.openapitools.jackson.nullable.JsonNullable;
1112
import java.time.OffsetDateTime;
1213
import javax.validation.Valid;
@@ -27,14 +28,14 @@
2728
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
2829
public class Order {
2930

30-
private Long id;
31+
private @Nullable Long id;
3132

32-
private Long petId;
33+
private @Nullable Long petId;
3334

34-
private Integer quantity;
35+
private @Nullable Integer quantity;
3536

3637
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
37-
private OffsetDateTime shipDate;
38+
private @Nullable OffsetDateTime shipDate;
3839

3940
/**
4041
* Order Status
@@ -73,7 +74,7 @@ public static StatusEnum fromValue(String value) {
7374
}
7475
}
7576

76-
private StatusEnum status;
77+
private @Nullable StatusEnum status;
7778

7879
private Boolean complete = false;
7980

0 commit comments

Comments
 (0)