diff --git a/src/main/java/io/swagger/codegen/v3/generators/DefaultCodegenConfig.java b/src/main/java/io/swagger/codegen/v3/generators/DefaultCodegenConfig.java index cd1009c5ba..f898c218d3 100644 --- a/src/main/java/io/swagger/codegen/v3/generators/DefaultCodegenConfig.java +++ b/src/main/java/io/swagger/codegen/v3/generators/DefaultCodegenConfig.java @@ -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())); diff --git a/src/main/java/io/swagger/codegen/v3/generators/java/AbstractJavaCodegen.java b/src/main/java/io/swagger/codegen/v3/generators/java/AbstractJavaCodegen.java index ebc1388158..11dd304657 100644 --- a/src/main/java/io/swagger/codegen/v3/generators/java/AbstractJavaCodegen.java +++ b/src/main/java/io/swagger/codegen/v3/generators/java/AbstractJavaCodegen.java @@ -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; @@ -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; @@ -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 @@ -515,6 +518,7 @@ public void processOpts() { setJakarta(Boolean.parseBoolean(String.valueOf(additionalProperties.get(JAKARTA)))); additionalProperties.put(JAKARTA, jakarta); } + } private void sanitizeConfig() { @@ -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) { diff --git a/src/main/java/io/swagger/codegen/v3/generators/java/SpringCodegen.java b/src/main/java/io/swagger/codegen/v3/generators/java/SpringCodegen.java index 29b7276a4f..968e672084 100644 --- a/src/main/java/io/swagger/codegen/v3/generators/java/SpringCodegen.java +++ b/src/main/java/io/swagger/codegen/v3/generators/java/SpringCodegen.java @@ -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"; @@ -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(); @@ -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 validationModeOptions = new HashMap(); + 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 @@ -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)); } @@ -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)) { @@ -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; } diff --git a/src/main/resources/handlebars/JavaSpring/NotUndefined.mustache b/src/main/resources/handlebars/JavaSpring/NotUndefined.mustache new file mode 100644 index 0000000000..be3adc1b42 --- /dev/null +++ b/src/main/resources/handlebars/JavaSpring/NotUndefined.mustache @@ -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[] payload() default {}; +} diff --git a/src/main/resources/handlebars/JavaSpring/NotUndefinedValidator.mustache b/src/main/resources/handlebars/JavaSpring/NotUndefinedValidator.mustache new file mode 100644 index 0000000000..92dc13018e --- /dev/null +++ b/src/main/resources/handlebars/JavaSpring/NotUndefinedValidator.mustache @@ -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{ + + @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; + } +} \ No newline at end of file diff --git a/src/main/resources/handlebars/JavaSpring/application.mustache b/src/main/resources/handlebars/JavaSpring/application.mustache index 5ef27edcf1..39ea0037d4 100644 --- a/src/main/resources/handlebars/JavaSpring/application.mustache +++ b/src/main/resources/handlebars/JavaSpring/application.mustache @@ -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 \ No newline at end of file diff --git a/src/main/resources/handlebars/JavaSpring/beanValidationFluentSetter.mustache b/src/main/resources/handlebars/JavaSpring/beanValidationFluentSetter.mustache new file mode 100644 index 0000000000..c3ef986297 --- /dev/null +++ b/src/main/resources/handlebars/JavaSpring/beanValidationFluentSetter.mustache @@ -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}} \ No newline at end of file diff --git a/src/main/resources/handlebars/JavaSpring/beanValidationGetter.mustache b/src/main/resources/handlebars/JavaSpring/beanValidationGetter.mustache new file mode 100644 index 0000000000..ff6ca48620 --- /dev/null +++ b/src/main/resources/handlebars/JavaSpring/beanValidationGetter.mustache @@ -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}} \ No newline at end of file diff --git a/src/main/resources/handlebars/JavaSpring/beanValidationSetter.mustache b/src/main/resources/handlebars/JavaSpring/beanValidationSetter.mustache new file mode 100644 index 0000000000..e38de32e7c --- /dev/null +++ b/src/main/resources/handlebars/JavaSpring/beanValidationSetter.mustache @@ -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}} \ No newline at end of file diff --git a/src/main/resources/handlebars/JavaSpring/beanValidationVariable.mustache b/src/main/resources/handlebars/JavaSpring/beanValidationVariable.mustache new file mode 100644 index 0000000000..ec3d29579a --- /dev/null +++ b/src/main/resources/handlebars/JavaSpring/beanValidationVariable.mustache @@ -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}} \ No newline at end of file diff --git a/src/main/resources/handlebars/JavaSpring/libraries/spring-boot/pom.mustache b/src/main/resources/handlebars/JavaSpring/libraries/spring-boot/pom.mustache index 54fbe99edd..1a18ebd250 100644 --- a/src/main/resources/handlebars/JavaSpring/libraries/spring-boot/pom.mustache +++ b/src/main/resources/handlebars/JavaSpring/libraries/spring-boot/pom.mustache @@ -120,9 +120,21 @@ 2.6.4 {{/threetenbp}} - {{#useBeanValidation}} - + {{#useBeanValidation}}{{#isStrictValidation}} + + org.openapitools + jackson-databind-nullable + 0.2.6 + + {{/isStrictValidation}}{{#isLooseValidation}} + + + org.openapitools + jackson-databind-nullable + 0.2.6 + + {{/isLooseValidation}} {{#jakarta}} jakarta.validation diff --git a/src/main/resources/handlebars/JavaSpring/libraries/spring-boot/swagger2SpringBoot.mustache b/src/main/resources/handlebars/JavaSpring/libraries/spring-boot/swagger2SpringBoot.mustache index 42167055ab..46fd67794d 100644 --- a/src/main/resources/handlebars/JavaSpring/libraries/spring-boot/swagger2SpringBoot.mustache +++ b/src/main/resources/handlebars/JavaSpring/libraries/spring-boot/swagger2SpringBoot.mustache @@ -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; @@ -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 { diff --git a/src/main/resources/handlebars/JavaSpring/libraries/spring-boot3/openAPISpringBoot.mustache b/src/main/resources/handlebars/JavaSpring/libraries/spring-boot3/openAPISpringBoot.mustache index 876248c766..03ce8621d8 100644 --- a/src/main/resources/handlebars/JavaSpring/libraries/spring-boot3/openAPISpringBoot.mustache +++ b/src/main/resources/handlebars/JavaSpring/libraries/spring-boot3/openAPISpringBoot.mustache @@ -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}}"}) @@ -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 { diff --git a/src/main/resources/handlebars/JavaSpring/libraries/spring-boot3/pom.mustache b/src/main/resources/handlebars/JavaSpring/libraries/spring-boot3/pom.mustache index ccd6595351..d1713dd3e7 100644 --- a/src/main/resources/handlebars/JavaSpring/libraries/spring-boot3/pom.mustache +++ b/src/main/resources/handlebars/JavaSpring/libraries/spring-boot3/pom.mustache @@ -50,8 +50,21 @@ jackson-dataformat-xml {{/withXml}} - {{#useBeanValidation}} +{{#useBeanValidation}}{{#isStrictValidation}} + + org.openapitools + jackson-databind-nullable + 0.2.6 + +{{/isStrictValidation}}{{#isLooseValidation}} + + + org.openapitools + jackson-databind-nullable + 0.2.6 + +{{/isLooseValidation}} jakarta.validation jakarta.validation-api diff --git a/src/main/resources/handlebars/JavaSpring/libraries/spring-cloud/pom.mustache b/src/main/resources/handlebars/JavaSpring/libraries/spring-cloud/pom.mustache index 6a68c8141c..7a7ca20511 100644 --- a/src/main/resources/handlebars/JavaSpring/libraries/spring-cloud/pom.mustache +++ b/src/main/resources/handlebars/JavaSpring/libraries/spring-cloud/pom.mustache @@ -111,9 +111,21 @@ 2.6.4 {{/threetenbp}} -{{#useBeanValidation}} - - {{#jakarta}} +{{#useBeanValidation}}{{#isStrictValidation}} + + + org.openapitools + jackson-databind-nullable + 0.2.6 + +{{/isStrictValidation}}{{#isLooseValidation}} + + + org.openapitools + jackson-databind-nullable + 0.2.6 + +{{/isLooseValidation}}{{#jakarta}} jakarta.validation jakarta.validation-api diff --git a/src/main/resources/handlebars/JavaSpring/libraries/spring-mvc/pom.mustache b/src/main/resources/handlebars/JavaSpring/libraries/spring-mvc/pom.mustache index 764393a6bd..5cb1579265 100644 --- a/src/main/resources/handlebars/JavaSpring/libraries/spring-mvc/pom.mustache +++ b/src/main/resources/handlebars/JavaSpring/libraries/spring-mvc/pom.mustache @@ -189,9 +189,21 @@ ${servlet-api-version} {{/jakarta}} -{{#useBeanValidation}} - - {{#jakarta}} +{{#useBeanValidation}}{{#isStrictValidation}} + + + org.openapitools + jackson-databind-nullable + 0.2.6 + +{{/isStrictValidation}}{{#isLooseValidation}} + + + org.openapitools + jackson-databind-nullable + 0.2.6 + +{{/isLooseValidation}}{{#jakarta}} jakarta.validation jakarta.validation-api diff --git a/src/main/resources/handlebars/JavaSpring/model.mustache b/src/main/resources/handlebars/JavaSpring/model.mustache index 681b07890c..16c0b0228d 100644 --- a/src/main/resources/handlebars/JavaSpring/model.mustache +++ b/src/main/resources/handlebars/JavaSpring/model.mustache @@ -9,6 +9,17 @@ import java.io.Serializable; {{/serializableModel}} {{#useBeanValidation}} import org.springframework.validation.annotation.Validated; +{{#isLooseValidation}} +import org.openapitools.jackson.nullable.JsonNullable; +import io.swagger.configuration.NotUndefined; +{{/isLooseValidation}} +{{#isStrictValidation}} +import org.openapitools.jackson.nullable.JsonNullable; +import io.swagger.configuration.NotUndefined; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +{{/isStrictValidation}} {{#jakarta}} import jakarta.validation.Valid; import jakarta.validation.constraints.*; diff --git a/src/main/resources/handlebars/JavaSpring/pojo.mustache b/src/main/resources/handlebars/JavaSpring/pojo.mustache index 8c87f3bb8f..f00a0b7406 100644 --- a/src/main/resources/handlebars/JavaSpring/pojo.mustache +++ b/src/main/resources/handlebars/JavaSpring/pojo.mustache @@ -3,6 +3,7 @@ */{{#description}} {{#useOas2}}@ApiModel{{/useOas2}}{{^useOas2}}@Schema{{/useOas2}}(description = "{{{description}}}"){{/description}} {{#useBeanValidation}}@Validated{{/useBeanValidation}} +{{#useBeanValidation}}{{#isStrictValidation}}@NotUndefined{{/isStrictValidation}}{{#isLooseValidation}}@NotUndefined{{/isLooseValidation}}{{/useBeanValidation}} {{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} {{#notNullJacksonAnnotation}}@JsonInclude(JsonInclude.Include.NON_NULL){{/notNullJacksonAnnotation}} @@ -34,12 +35,11 @@ public class {{classname}} {{#parent}}extends {{{parent}}}{{/parent}} {{#seriali private {{{datatypeWithEnum}}} {{name}}{{#required}} = {{{defaultValue}}}{{/required}}{{^required}} = null{{/required}}; {{/isContainer}} {{^isContainer}} - private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}}; +{{#useBeanValidation}}{{>beanValidationVariable}}{{/useBeanValidation}}{{^useBeanValidation}} private {{{datatypeWithEnum}}} {{name}} = {{{defaultValue}}};{{/useBeanValidation}} {{/isContainer}} - {{/vars}} {{#vars}} - public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { +{{#useBeanValidation}}{{>beanValidationFluentSetter}}{{/useBeanValidation}}{{^useBeanValidation}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{/useBeanValidation}} this.{{name}} = {{name}}; return this; } @@ -86,20 +86,14 @@ public class {{classname}} {{#parent}}extends {{{parent}}}{{/parent}} {{#seriali {{#vendorExtensions.extraAnnotation}} {{{vendorExtensions.extraAnnotation}}} {{/vendorExtensions.extraAnnotation}} - {{#useOas2}} - @ApiModelProperty({{#example}}example = "{{{example}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}") - {{/useOas2}} - {{^useOas2}} - @Schema({{#example}}example = "{{{example}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}accessMode = Schema.AccessMode.READ_ONLY, {{/isReadOnly}}description = "{{{description}}}") - {{/useOas2}} - {{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}} public {{{datatypeWithEnum}}} {{getter}}() { + {{#useOas2}}@ApiModelProperty({{#example}}example = "{{{example}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/useOas2}} + {{^useOas2}}@Schema({{#example}}example = "{{{example}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}accessMode = Schema.AccessMode.READ_ONLY, {{/isReadOnly}}description = "{{{description}}}"){{/useOas2}} + {{#useBeanValidation}}{{>beanValidationGetter}}{{/useBeanValidation}}{{^useBeanValidation}}public {{{datatypeWithEnum}}} {{getter}}() { {{/useBeanValidation}} return {{name}}; } - - public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { +{{#useBeanValidation}}{{>beanValidationSetter}}{{/useBeanValidation}}{{^useBeanValidation}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { {{/useBeanValidation}} this.{{name}} = {{name}}; } - {{/vars}} @Override diff --git a/src/test/java/io/swagger/codegen/v3/generators/java/SpringGeneratorTest.java b/src/test/java/io/swagger/codegen/v3/generators/java/SpringGeneratorTest.java new file mode 100644 index 0000000000..0d66ade7c0 --- /dev/null +++ b/src/test/java/io/swagger/codegen/v3/generators/java/SpringGeneratorTest.java @@ -0,0 +1,115 @@ +package io.swagger.codegen.v3.generators.java; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.codegen.v3.generators.AbstractCodegenTest; +import io.swagger.codegen.v3.service.GenerationRequest; +import io.swagger.codegen.v3.service.GeneratorService; +import io.swagger.codegen.v3.service.Options; +import io.swagger.util.Json; +import io.swagger.util.Yaml; +import org.apache.commons.io.IOUtils; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.List; + +public class SpringGeneratorTest extends AbstractCodegenTest { + @Test + public void testGenerator() throws Exception { + + String path = getOutFolder(false).getAbsolutePath(); + GenerationRequest request = new GenerationRequest(); + request + .codegenVersion(GenerationRequest.CodegenVersion.V3) // use V2 to target Swagger/OpenAPI 2.x Codegen version + .type(GenerationRequest.Type.CLIENT) + .lang("spring") + // .specURL("https://petstore3.swagger.io/api/v3/openapi.yaml") + .spec(loadSpecAsNode( "3_0_0/nullable-required.yaml", + true, // YAML file, use false for JSON + false)) // OpenAPI 3.x - use true for Swagger/OpenAPI 2.x definitions + .options( + new Options() + // .addAdditionalProperty("validationMode", "loose") + // .addAdditionalProperty("validationMode", "legacy") + // .addAdditionalProperty("validationMode", "legacyNullable") + // .addAdditionalProperty("useBeanValidation", false) + // .addAdditionalProperty("useNullableForNotNull", false) + .outputDir(path) + ); + + List files = new GeneratorService().generationRequest(request).generate(); + Assert.assertFalse(files.isEmpty()); +/* for (File f: files) { + // test stuff + if (f.getName().endsWith("Pet.java")) { + String content = new String(Files.readAllBytes(f.toPath())); + System.out.println(content); + } + if (f.getName().endsWith("application.properties")) { + String content = new String(Files.readAllBytes(f.toPath())); + System.out.println(content); + } + if (f.getName().endsWith("pom.xml")) { + String content = new String(Files.readAllBytes(f.toPath())); + // System.out.println(content); + } + if (f.getName().endsWith("Boot.java")) { + String content = new String(Files.readAllBytes(f.toPath())); + // System.out.println(content); + } + }*/ + } + + protected static File getTmpFolder() { + try { + File outputFolder = Files.createTempFile("codegentest-", "-tmp").toFile(); + outputFolder.delete(); + outputFolder.mkdir(); + outputFolder.deleteOnExit(); + return outputFolder; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + protected static File getOutFolder(boolean delete) { + try { + File outputFolder = getTmpFolder(); + + System.out.println(outputFolder.getAbsolutePath()); + if (delete) { + // delete.. + } + return outputFolder; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + protected JsonNode loadSpecAsNode(final String file, boolean yaml, boolean v2) { + InputStream in = null; + String s = ""; + try { + in = getClass().getClassLoader().getResourceAsStream(file); + if (yaml) { + if (v2) { + return Yaml.mapper().readTree(in); + } else { + return io.swagger.v3.core.util.Yaml.mapper().readTree(in); + } + } + if (v2) { + return Json.mapper().readTree(in); + } + return io.swagger.v3.core.util.Json.mapper().readTree(in); + } catch (Exception e) { + throw new RuntimeException("could not load file " + file); + } finally { + IOUtils.closeQuietly(in); + } + } +} diff --git a/src/test/resources/3_0_0/nullable-required.yaml b/src/test/resources/3_0_0/nullable-required.yaml new file mode 100644 index 0000000000..c20a1d56b4 --- /dev/null +++ b/src/test/resources/3_0_0/nullable-required.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.2 +info: + title: Nullable Required + version: 1.0.0 +servers: + - url: /api/v3 +paths: + /pet: + post: + operationId: addPet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' +components: + schemas: + Pet: + required: + - notNullable_required + - nullable_required + type: object + properties: + nullable_notRequired: + type: string + example: doggie + nullable: true + notNullable_notRequired: + type: string + example: doggie + notNullable_required: + type: string + example: doggie + nullable_required: + type: string + example: doggie + nullable: true