Skip to content

Commit

Permalink
Spring - update nullable and required validation
Browse files Browse the repository at this point in the history
Co-authored-by: frantuma <[email protected]>
  • Loading branch information
micryc and frantuma committed Aug 27, 2024
1 parent 7879271 commit 2835d9d
Show file tree
Hide file tree
Showing 20 changed files with 468 additions and 25 deletions.
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
{{#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}}


@SpringBootApplication
@ComponentScan(basePackages = { "{{basePackage}}", "{{apiPackage}}" , "{{configPackage}}"})
Expand All @@ -26,6 +39,20 @@ public class OpenAPISpringBoot implements CommandLineRunner {
public static void main(String[] args) throws Exception {
new SpringApplication(OpenAPISpringBoot.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 WebMvcConfigurationSupport {
Expand Down
Loading

0 comments on commit 2835d9d

Please sign in to comment.