Skip to content

Commit

Permalink
Adds @nullability annotation to Spring Boot generator
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
slobodator committed Dec 18, 2024
1 parent 289425b commit b84983a
Show file tree
Hide file tree
Showing 695 changed files with 2,702 additions and 1,921 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ public void processOpts() {
convertPropertyToStringAndWriteBack(RESOURCE_FOLDER, this::setResourceFolder);

typeMapping.put("file", "org.springframework.core.io.Resource");
importMapping.put("Nullable", "org.springframework.lang.Nullable");
importMapping.put("org.springframework.core.io.Resource", "org.springframework.core.io.Resource");
importMapping.put("DateTimeFormat", "org.springframework.format.annotation.DateTimeFormat");
importMapping.put("ApiIgnore", "springfox.documentation.annotations.ApiIgnore");
Expand Down Expand Up @@ -952,6 +953,8 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
if (model.getVendorExtensions().containsKey("x-jackson-optional-nullable-helpers")) {
model.imports.add("Arrays");
}

model.imports.add("Nullable");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{^required}}{{^useOptional}}{{#openApiNullable}}{{^isNullable}}@Nullable {{/isNullable}}{{/openApiNullable}}{{^openApiNullable}}@Nullable {{/openApiNullable}}{{/useOptional}}{{/required}}
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}
{{#isContainer}}
{{#useBeanValidation}}@Valid{{/useBeanValidation}}
{{#openApiNullable}}
private {{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}
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}}
{{/openApiNullable}}
{{^openApiNullable}}
private {{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
{{/openApiNullable}}
{{/isContainer}}
{{^isContainer}}
Expand All @@ -86,10 +86,10 @@ public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
{{/isDateTime}}
{{#openApiNullable}}
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}}
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}}
{{/openApiNullable}}
{{^openApiNullable}}
private {{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
{{/openApiNullable}}
{{/isContainer}}
{{/vars}}
Expand Down Expand Up @@ -130,7 +130,7 @@ public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}
/**
* Constructor with all args parameters
*/
public {{classname}}({{#vendorExtensions.x-java-all-args-constructor-vars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-java-all-args-constructor-vars}}) {
public {{classname}}({{#vendorExtensions.x-java-all-args-constructor-vars}}{{>nullableAnnotation}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-java-all-args-constructor-vars}}) {
{{#parent}}
super({{#parentVars}}{{name}}{{^-last}}, {{/-last}}{{/parentVars}});
{{/parent}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,18 @@ public PropertyAssert withType(final String expectedType) {
public PropertyAnnotationsAssert assertPropertyAnnotations() {
return new PropertyAnnotationsAssert(this, actual.getAnnotations());
}

public PropertyAnnotationsAssert doesNotHaveAnnotation(String annotationName) {
return new PropertyAnnotationsAssert(
this,
actual.getAnnotations()
).doesNotContainWithName(annotationName);
}

public PropertyAnnotationsAssert hasAnnotation(String annotationName) {
return new PropertyAnnotationsAssert(
this,
actual.getAnnotations()
).containsWithName(annotationName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4859,7 +4859,7 @@ public void optionalListShouldBeEmpty() throws IOException {
.collect(Collectors.toMap(File::getName, Function.identity()));

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

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

JavaFileAssert.assertThat(files.get("PetDto.java"))
.fileContains("private List<@Valid TagDto> tags")
.fileContains("private List<@Valid TagDto> tagsDefaultList = new ArrayList<>()")
.fileContains("private Set<@Valid TagDto> tagsUnique")
.fileContains("private Set<@Valid TagDto> tagsDefaultSet = new LinkedHashSet<>();")
.fileContains("private List<String> stringList")
.fileContains("private List<String> stringDefaultList = new ArrayList<>(Arrays.asList(\"A\", \"B\"));")
.fileContains("private List<String> stringEmptyDefaultList = new ArrayList<>();")
.fileContains("Set<String> stringSet")
.fileContains("private Set<String> stringDefaultSet = new LinkedHashSet<>(Arrays.asList(\"A\", \"B\"));")
.fileContains("private Set<String> stringEmptyDefaultSet = new LinkedHashSet<>();")
.fileDoesNotContain("private List<@Valid TagDto> tags = new ArrayList<>()")
.fileDoesNotContain("private Set<@Valid TagDto> tagsUnique = new LinkedHashSet<>()")
.fileDoesNotContain("private List<String> stringList = new ArrayList<>()")
.fileDoesNotContain("private Set<String> stringSet = new LinkedHashSet<>()");
.fileContains("private @Nullable List<@Valid TagDto> tags")
.fileContains("private @Nullable List<@Valid TagDto> tagsDefaultList = new ArrayList<>()")
.fileContains("private @Nullable Set<@Valid TagDto> tagsUnique")
.fileContains("private @Nullable Set<@Valid TagDto> tagsDefaultSet = new LinkedHashSet<>();")
.fileContains("private @Nullable List<String> stringList")
.fileContains("private @Nullable List<String> stringDefaultList = new ArrayList<>(Arrays.asList(\"A\", \"B\"));")
.fileContains("private @Nullable List<String> stringEmptyDefaultList = new ArrayList<>();")
.fileContains("@Nullable Set<String> stringSet")
.fileContains("private @Nullable Set<String> stringDefaultSet = new LinkedHashSet<>(Arrays.asList(\"A\", \"B\"));")
.fileContains("private @Nullable Set<String> stringEmptyDefaultSet = new LinkedHashSet<>();")
.fileDoesNotContain("List<@Valid TagDto> tags = new ArrayList<>()")
.fileDoesNotContain("Set<@Valid TagDto> tagsUnique = new LinkedHashSet<>()")
.fileDoesNotContain("List<String> stringList = new ArrayList<>()")
.fileDoesNotContain("Set<String> stringSet = new LinkedHashSet<>()");
}

@Test
Expand Down Expand Up @@ -5099,4 +5099,63 @@ public void testEnumUnknownDefaultCaseDeserializationNotSet_issue13241() throws
.assertMethod("build")
.doesNotHaveAnnotation("Deprecated");
}

@Test
public void shouldAnnotateNonRequiredFieldsAsNullable() throws IOException {
SpringCodegen codegen = new SpringCodegen();
codegen.setLibrary(SPRING_BOOT);

Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
var file = files.get("Item.java");

JavaFileAssert.assertThat(file)
.assertProperty("mandatoryName")
.doesNotHaveAnnotation("Nullable");
JavaFileAssert.assertThat(file)
.assertProperty("optionalDescription")
.hasAnnotation("Nullable");
JavaFileAssert.assertThat(file)
.assertProperty("nullableStr")
.doesNotHaveAnnotation("Nullable");
}

@Test
public void shouldNotAnnotateNonRequiredFieldsAsNullableWhileUseOptional() throws IOException {
SpringCodegen codegen = new SpringCodegen();
codegen.setLibrary(SPRING_BOOT);
codegen.setUseOptional(true);

Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
var file = files.get("Item.java");

JavaFileAssert.assertThat(file)
.assertProperty("mandatoryName")
.doesNotHaveAnnotation("Nullable");
JavaFileAssert.assertThat(file)
.assertProperty("optionalDescription")
.doesNotHaveAnnotation("Nullable");
JavaFileAssert.assertThat(file)
.assertProperty("nullableStr")
.doesNotHaveAnnotation("Nullable");
}

@Test
public void shouldNotAnnotateNonRequiredFieldsAsNullableWhileNotUsingOpenApiNullable() throws IOException {
SpringCodegen codegen = new SpringCodegen();
codegen.setLibrary(SPRING_BOOT);
codegen.setOpenApiNullable(false);

Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/nullable-annotation.yaml");
var file = files.get("Item.java");

JavaFileAssert.assertThat(file)
.assertProperty("mandatoryName")
.doesNotHaveAnnotation("Nullable");
JavaFileAssert.assertThat(file)
.assertProperty("optionalDescription")
.hasAnnotation("Nullable");
JavaFileAssert.assertThat(file)
.assertProperty("nullableStr")
.hasAnnotation("Nullable");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
openapi: 3.0.0
components:
schemas:
Item:
type: object
required:
- mandatoryName
properties:
mandatoryName:
type: String
optionalDescription:
type: string
nullableStr:
type: String
nullable: true
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand All @@ -22,9 +23,9 @@
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
public class Addressable {

private String href;
private @Nullable String href;

private String id;
private @Nullable String id;

public Addressable href(String href) {
this.href = href;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.openapitools.model.Entity;
import org.openapitools.model.FooRefOrValue;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand All @@ -29,11 +30,11 @@ public class Bar extends Entity implements BarRefOrValue {

private String id;

private String barPropA;
private @Nullable String barPropA;

private String fooPropB;
private @Nullable String fooPropB;

private FooRefOrValue foo;
private @Nullable FooRefOrValue foo;

public Bar() {
super();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.openapitools.model.Entity;
import org.openapitools.model.FooRefOrValue;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand All @@ -29,11 +30,11 @@
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
public class BarCreate extends Entity {

private String barPropA;
private @Nullable String barPropA;

private String fooPropB;
private @Nullable String fooPropB;

private FooRefOrValue foo;
private @Nullable FooRefOrValue foo;

public BarCreate() {
super();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.openapitools.model.EntityRef;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.openapitools.model.Bar;
import org.openapitools.model.BarRef;
import org.openapitools.model.FooRefOrValue;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand Down Expand Up @@ -38,13 +39,13 @@
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
public class Entity {

private String href;
private @Nullable String href;

private String id;
private @Nullable String id;

private String atSchemaLocation;
private @Nullable String atSchemaLocation;

private String atBaseType;
private @Nullable String atBaseType;

private String atType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand Down Expand Up @@ -35,17 +36,17 @@
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
public class EntityRef {

private String name;
private @Nullable String name;

private String atReferredType;
private @Nullable String atReferredType;

private String href;
private @Nullable String href;

private String id;
private @Nullable String id;

private String atSchemaLocation;
private @Nullable String atSchemaLocation;

private String atBaseType;
private @Nullable String atBaseType;

private String atType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand All @@ -21,9 +22,9 @@
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
public class Extensible {

private String atSchemaLocation;
private @Nullable String atSchemaLocation;

private String atBaseType;
private @Nullable String atBaseType;

private String atType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.openapitools.model.Entity;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
Expand All @@ -26,9 +27,9 @@
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.11.0-SNAPSHOT")
public class Foo extends Entity implements FooRefOrValue {

private String fooPropA;
private @Nullable String fooPropA;

private String fooPropB;
private @Nullable String fooPropB;

public Foo() {
super();
Expand Down
Loading

0 comments on commit b84983a

Please sign in to comment.