Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring - update nullable and required validation #1308

Merged
merged 2 commits into from
Aug 27, 2024
Merged
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
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,11 @@
<scala-version>2.11.1</scala-version>
<felix-version>3.3.0</felix-version>
<commons-io-version>2.16.1</commons-io-version>
<commons-cli-version>1.8.0</commons-cli-version>
<commons-cli-version>1.9.0</commons-cli-version>
<junit-version>4.13.2</junit-version>
<maven-plugin-version>1.0.0</maven-plugin-version>
<commons-lang-version>3.14.0</commons-lang-version>
<slf4j-version>1.7.36</slf4j-version>
<commons-lang-version>3.16.0</commons-lang-version>
<slf4j-version>2.0.9</slf4j-version>
<scala-maven-plugin-version>3.2.1</scala-maven-plugin-version>
<testng-version>7.10.2</testng-version>
<surefire-version>3.3.0</surefire-version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1615,6 +1615,7 @@ protected void setSchemaProperties(String name, CodegenProperty codegenProperty,
codegenProperty.defaultValue = toDefaultValue(schema);
codegenProperty.defaultValueWithParam = toDefaultValueWithParam(name, schema);
codegenProperty.jsonSchema = Json.pretty(schema);
codegenProperty.schemaType = schema.getType();
codegenProperty.nullable = Boolean.TRUE.equals(schema.getNullable());
codegenProperty.getVendorExtensions().put(CodegenConstants.IS_NULLABLE_EXT_NAME, Boolean.TRUE.equals(schema.getNullable()));
codegenProperty.getVendorExtensions().put(IS_NULLABLE_FALSE, Boolean.FALSE.equals(schema.getNullable()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.swagger.codegen.v3.generators.DefaultCodegenConfig;
import io.swagger.codegen.v3.generators.features.NotNullAnnotationFeatures;
import io.swagger.codegen.v3.generators.handlebars.java.JavaHelper;
import io.swagger.codegen.v3.utils.URLPathUtil;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
Expand All @@ -32,6 +33,7 @@
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -209,6 +211,7 @@ public AbstractJavaCodegen() {
cliOptions.add(jeeSpec);

cliOptions.add(CliOption.newBoolean(USE_NULLABLE_FOR_NOTNULL, "Add @NotNull depending on `nullable` property instead of `required`"));

}

@Override
Expand Down Expand Up @@ -515,6 +518,7 @@ public void processOpts() {
setJakarta(Boolean.parseBoolean(String.valueOf(additionalProperties.get(JAKARTA))));
additionalProperties.put(JAKARTA, jakarta);
}

}

private void sanitizeConfig() {
Expand Down Expand Up @@ -1155,6 +1159,11 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
operation.addExtension("x-accepts", accepts);
}
}
final URL urlInfo = URLPathUtil.getServerURL(openAPI);
if (urlInfo != null && StringUtils.isNotBlank(urlInfo.getPath())) {
additionalProperties.put("contextPathWithoutHost", urlInfo.getPath());
}

}

private static String getAccept(Operation operation) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,14 @@ public class SpringCodegen extends AbstractJavaCodegen implements BeanValidation
public static final String SPRING_BOOT_VERSION_2 = "springBootV2";
public static final String DATE_PATTERN = "datePattern";
public static final String DATE_TIME_PATTERN = "dateTimePattern";

public static final String THROWS_EXCEPTION = "throwsException";

public static final String VALIDATION_MODE_OPTION = "validationMode";
public static final String VALIDATION_MODE_LEGACY = "legacy";
public static final String VALIDATION_MODE_LEGACY_NULLABLE = "legacyNullable";
public static final String VALIDATION_MODE_STRICT = "strict";
public static final String VALIDATION_MODE_LOOSE = "loose";

protected String title = "swagger-petstore";
protected String configPackage = "io.swagger.configuration";
protected String basePackage = "io.swagger";
Expand All @@ -92,6 +97,7 @@ public class SpringCodegen extends AbstractJavaCodegen implements BeanValidation
protected String springBootVersion = "2.1.16.RELEASE";
protected boolean throwsException = false;
private boolean notNullJacksonAnnotation = false;
protected String validationMode = "strict";

public SpringCodegen() {
super();
Expand Down Expand Up @@ -146,6 +152,15 @@ public SpringCodegen() {
springBootVersionOption.setEnum(springBootEnum);
cliOptions.add(springBootVersionOption);

CliOption validationMode = new CliOption(VALIDATION_MODE_OPTION, "Validation mode to apply");
validationMode.setDefault(VALIDATION_MODE_STRICT);
Map<String, String> validationModeOptions = new HashMap<String, String>();
validationModeOptions.put(VALIDATION_MODE_STRICT, "Use Helper JsonNullable/NotUndefined on required+nullable fields, @NotNull on required, jackson validation on default");
validationModeOptions.put(VALIDATION_MODE_LOOSE, "Use Helper JsonNullable/NotUndefined on required+nullable fields, @NotNull on required, no validation on default");
validationModeOptions.put(VALIDATION_MODE_LEGACY, "Apply @NotNull on required fields");
validationModeOptions.put(VALIDATION_MODE_LEGACY_NULLABLE, "Apply @NotNull when nullable is not defined or false, if useNullableForNotNull=false Apply @NotNull on required fields");
validationMode.setEnum(validationModeOptions);
cliOptions.add(validationMode);
}

@Override
Expand Down Expand Up @@ -232,6 +247,11 @@ public void processOpts() {
this.setTitle((String) additionalProperties.get(TITLE));
}

if (additionalProperties.containsKey(VALIDATION_MODE_OPTION)) {
this.setValidationMode((String) additionalProperties.get(VALIDATION_MODE_OPTION));
}
additionalProperties.put("is" + validationMode.substring(0, 1).toUpperCase() + validationMode.substring(1) + "Validation", true);

if (additionalProperties.containsKey(CONFIG_PACKAGE)) {
this.setConfigPackage((String) additionalProperties.get(CONFIG_PACKAGE));
}
Expand Down Expand Up @@ -297,6 +317,12 @@ public void processOpts() {

if (useBeanValidation) {
writePropertyBack(USE_BEANVALIDATION, useBeanValidation);
if (VALIDATION_MODE_LOOSE.equals(validationMode) || VALIDATION_MODE_STRICT.equals(validationMode)) {
supportingFiles.add(new SupportingFile("NotUndefined.mustache",
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "NotUndefined.java"));
supportingFiles.add(new SupportingFile("NotUndefinedValidator.mustache",
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "NotUndefinedValidator.java"));
}
}

if (additionalProperties.containsKey(IMPLICIT_HEADERS)) {
Expand Down Expand Up @@ -845,6 +871,10 @@ public void setConfigPackage(String configPackage) {
this.configPackage = configPackage;
}

public void setValidationMode(String validationMode) {
this.validationMode = validationMode;
}

public void setBasePackage(String configPackage) {
this.basePackage = configPackage;
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/resources/handlebars/JavaSpring/NotUndefined.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package {{configPackage}};

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = NotUndefinedValidator.class)
public @interface NotUndefined {
String message() default "field cannot be undefined";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package {{configPackage}};

import org.openapitools.jackson.nullable.JsonNullable;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Field;

public class NotUndefinedValidator implements ConstraintValidator<NotUndefined, Object>{
@Override
public void initialize(NotUndefined constraintAnnotation) {
}

@Override
public boolean isValid(Object addressInformation, ConstraintValidatorContext context) {
Class<?> objClass = addressInformation.getClass();
Field[] fields = objClass.getDeclaredFields();
for (Field field : fields) {
if (field.getType().equals(JsonNullable.class)){
field.setAccessible(true);
try {
Object value = field.get(addressInformation);
if(value.equals(JsonNullable.undefined())){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(field.getName() + " cannot be undefined")
.addConstraintViolation();
return false;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return true;
}
}
4 changes: 2 additions & 2 deletions src/main/resources/handlebars/JavaSpring/application.mustache
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{{#useOas2}}
springfox.documentation.swagger.v2.path=/api-docs
server.contextPath={{^contextPath}}/{{/contextPath}}{{#contextPath}}{{contextPath}}{{/contextPath}}
server.contextPath={{^contextPathWithoutHost}}/{{/contextPathWithoutHost}}{{#contextPathWithoutHost}}{{contextPathWithoutHost}}{{/contextPathWithoutHost}}
{{/useOas2}}
{{^useOas2}}
springdoc.api-docs.path=/api-docs
{{/useOas2}}
server.servlet.contextPath={{^contextPath}}/{{/contextPath}}{{#contextPath}}{{contextPath}}{{/contextPath}}
server.servlet.contextPath={{^contextPathWithoutHost}}/{{/contextPathWithoutHost}}{{#contextPathWithoutHost}}{{contextPathWithoutHost}}{{/contextPathWithoutHost}}
server.port={{serverPort}}
spring.jackson.date-format={{basePackage}}.RFC3339DateFormat
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{#isLegacyValidation}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{/isLegacyValidation}}{{#isLegacyNullableValidation}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{/isLegacyNullableValidation}}
{{#isStrictValidation}}{{#required}}{{#nullable}} public {{classname}} {{name}}(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { {{/nullable}}{{^nullable}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{/nullable}}{{/required}}{{^required}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{/required}}{{/isStrictValidation}}
{{#isLooseValidation}}{{#required}}{{#nullable}} public {{classname}} {{name}}(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { {{/nullable}}{{^nullable}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{/nullable}}{{/required}}{{^required}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{/required}}{{/isLooseValidation}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{{#isLegacyValidation}}{{#required}}@NotNull{{/required}} {{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
{{>beanValidationCore}} public {{{datatypeWithEnum}}} {{getter}}() { {{/isLegacyValidation}}{{#isLegacyNullableValidation}}{{#required}}{{^useNullableForNotNull}}@NotNull{{/useNullableForNotNull}}{{/required}}
{{#useNullableForNotNull}}{{^nullable}}@NotNull{{/nullable}}{{/useNullableForNotNull}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
{{>beanValidationCore}} public {{{datatypeWithEnum}}} {{getter}}() { {{/isLegacyNullableValidation}}{{#isStrictValidation}}{{#required}}{{#nullable}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
{{>beanValidationCore}} public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}() { {{/nullable}}{{^nullable}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
@NotNull
{{>beanValidationCore}} public {{{datatypeWithEnum}}} {{getter}}() { {{/nullable}}{{/required}}{{^required}}{{#nullable}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
{{>beanValidationCore}} public {{{datatypeWithEnum}}} {{getter}}() {
{{/nullable}}{{^nullable}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
{{>beanValidationCore}} public {{{datatypeWithEnum}}} {{getter}}() { {{/nullable}}{{/required}}{{/isStrictValidation}} {{#isLooseValidation}}{{#required}}{{#nullable}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
{{>beanValidationCore}} public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}() { {{/nullable}}{{^nullable}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
@NotNull
{{>beanValidationCore}} public {{{datatypeWithEnum}}} {{getter}}() { {{/nullable}}{{/required}}{{^required}}{{#nullable}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
{{>beanValidationCore}} public {{{datatypeWithEnum}}} {{getter}}() { {{/nullable}}{{^nullable}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{#isNotContainer}}{{^isPrimitiveType}}
@Valid{{/isPrimitiveType}}{{/isNotContainer}}
{{>beanValidationCore}} public {{{datatypeWithEnum}}} {{getter}}() { {{/nullable}}{{/required}}{{/isLooseValidation}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{#isLegacyValidation}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}){ {{/isLegacyValidation}}
{{#isLegacyNullableValidation}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}){ {{/isLegacyNullableValidation}}
{{#isStrictValidation}}
{{#required}}{{#nullable}} public void {{setter}}(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { {{/nullable}}
{{^nullable}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { {{/nullable}}{{/required}}
{{^required}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { {{/required}}{{/isStrictValidation}}{{#isLooseValidation}}
{{#required}}{{#nullable}} public void {{setter}}(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { {{/nullable}}
{{^nullable}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { {{/nullable}}{{/required}}
{{^required}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { {{/required}}{{/isLooseValidation}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{{#isLegacyValidation}} private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};{{/isLegacyValidation}}{{#isLegacyNullableValidation}} private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};{{/isLegacyNullableValidation}}
{{#isStrictValidation}}
{{#required}}
{{#nullable}}
private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.undefined();
{{/nullable}}
{{^nullable}}
private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};
{{/nullable}}
{{/required}}
{{^required}}
{{#nullable}}
private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};
{{/nullable}}
{{^nullable}}
@JsonInclude(JsonInclude.Include.NON_ABSENT) // Exclude from JSON if absent
@JsonSetter(nulls = Nulls.FAIL) // FAIL setting if the value is null
private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};
{{/nullable}}
{{/required}}
{{/isStrictValidation}}
{{#isLooseValidation}}
{{#required}}
{{#nullable}}
private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.undefined();
{{/nullable}}
{{^nullable}}
private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};
{{/nullable}}
{{/required}}
{{^required}}
{{#nullable}}
private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};
{{/nullable}}
{{^nullable}}
private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};
{{/nullable}}
{{/required}}
{{/isLooseValidation}}
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,21 @@
<version>2.6.4</version>
</dependency>
{{/threetenbp}}
{{#useBeanValidation}}

{{#useBeanValidation}}{{#isStrictValidation}}
<!-- Bean Validation API support -->
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
{{/isStrictValidation}}{{#isLooseValidation}}
<!-- Bean Validation API support -->
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
{{/isLooseValidation}}
{{#jakarta}}
<dependency>
<groupId>jakarta.validation</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
{{#useBeanValidation}}
{{#isStrictValidation}}
import org.openapitools.jackson.nullable.JsonNullableModule;
import org.springframework.context.annotation.Bean;
import com.fasterxml.jackson.databind.Module;
{{/isStrictValidation}}
{{#isLooseValidation}}
import org.openapitools.jackson.nullable.JsonNullableModule;
import org.springframework.context.annotation.Bean;
import com.fasterxml.jackson.databind.Module;
{{/isLooseValidation}}
{{/useBeanValidation}}

{{#useOas2}}
import springfox.documentation.swagger2.annotations.EnableSwagger2;
Expand All @@ -34,6 +46,20 @@ public class Swagger2SpringBoot implements CommandLineRunner {
public static void main(String[] args) throws Exception {
new SpringApplication(Swagger2SpringBoot.class).run(args);
}
{{#useBeanValidation}}
{{#isStrictValidation}}
@Bean
public Module jsonNullableModule() {
return new JsonNullableModule();
}
{{/isStrictValidation}}
{{#isLooseValidation}}
@Bean
public Module jsonNullableModule() {
return new JsonNullableModule();
}
{{/isLooseValidation}}
{{/useBeanValidation}}

@Configuration
static class CustomDateConfig extends WebMvcConfigurerAdapter {
Expand Down
Loading
Loading