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 26, 2024
1 parent f6cea77 commit 0d8661f
Show file tree
Hide file tree
Showing 21 changed files with 470 additions and 27 deletions.
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<groupId>io.swagger.codegen.v3</groupId>
<artifactId>swagger-codegen-generators</artifactId>
<version>1.0.51</version>
<version>1.0.52-SNAPSHOT</version>
<packaging>jar</packaging>

<build>
Expand Down Expand Up @@ -262,7 +262,7 @@
</dependencies>
<properties>
<maven.compiler.release>8</maven.compiler.release>
<swagger-codegen-version>3.0.61</swagger-codegen-version>
<swagger-codegen-version>3.0.62-SNAPSHOT</swagger-codegen-version>
<swagger-parser-version>2.1.22</swagger-parser-version>
<swagger-core-version>2.2.21</swagger-core-version>
<jackson-version>2.17.0</jackson-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

0 comments on commit 0d8661f

Please sign in to comment.