I'm conducting tests to validate when a field in a request body is not received, and I expect an exception to be thrown. However, Spring doesn't even return the generic error. I've generated classes from an OpenAPI
contract and am reusing the generated controller methods. Here are the details:
Generated Controller:
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
@Validated
@Tag(name = "user", description = "Operations about user")
public interface UserApi {
@Operation(
operationId = "createUser",
summary = "Create user",
description = "This can only be done by the logged-in user.",
tags = { "user" },
responses = {
@ApiResponse(responseCode = "default", description = "successful operation", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = User.class)),
@Content(mediaType = "application/xml", schema = @Schema(implementation = User.class))
})
}
)
@RequestMapping(
method = RequestMethod.POST,
value = "/user",
produces = { "application/json", "application/xml" },
consumes = { "application/json", "application/xml", "application/x-www-form-urlencoded" }
)
ResponseEntity<User> createUser(
@Parameter(name = "User", description = "Created user object", required = true) @Valid @RequestBody User user
);
}
Generated Entity:
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
public class User {
...
private String documentNumber;
...
public User documentNumber(String documentNumber) {
this.documentNumber = documentNumber;
return this;
}
/**
* Get documentNumber
* @return documentNumber
*/
@NotNull
@Schema(name = "documentNumber", example = "John", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("documentNumber")
public String getDocumentNumber() {
return documentNumber;
}
public void setDocumentNumber(String documentNumber) {
this.documentNumber = documentNumber;
}
...
}
Controller Created by Me:
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/hello")
public class UserController implements UserApi {
private final UserService userService;
private final MapperUtils mapperUtils;
@Override
@PostMapping(
value = "/user",
produces = { "application/json" }
)
public ResponseEntity<User> createUser(User user) {
return ResponseEntity.ok(mapperUtils.mapRequestToUser(mapperUtils.mapUserToResponse(user)));
}
}
Entity Created by Me:
@Data
@Table(name = "USUARIO")
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity {
@Id
private Long id;
private String name;
private String documentNumber;
private String email;
private Integer state;
}
As you can see, in the generated entity, the documentNumber
field is marked as required with the @NotNull
annotation. Also, in the generated controller, the @Valid
and @Validated
annotations are present. Therefore, it should return a validation error. However, the flow continues, and it records the data I send, including the null values. What is happening? Dependencies File Shared.
<!-- XML content provided for Maven POM file -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- Project information -->
<modelVersion>4.0.0</modelVersion>
<parent>
<!-- Spring Boot parent -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo-spring-maven</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-spring-maven</name>
<description>Demo project for Spring Boot</description>
<!-- Project properties including OpenAPI generator version, Swagger version, Jackson version, etc. -->
<properties>
<openapi-generator.version>6.6.0</openapi-generator.version>
<openapi.package.api>org.example.api.spec.controller</openapi.package.api>
<openapi.package.model>org.example.api.spec.dto</openapi.package.model>
<swagger.annotations.version>2.2.14</swagger.annotations.version>
<jackson-nullable.version>0.2.6</jackson-nullable.version>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- Project dependencies including Spring Boot, H2 database, validation-api, Swagger, OpenAPI tools, etc. -->
<dependencies>
<!-- Spring Boot Web starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>compile</scope>
</dependency>
<!-- Validation API -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- Swagger Annotations -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger.annotations.version}</version>
</dependency>
<!-- OpenAPI tools Jackson Databind Nullable -->
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>${jackson-nullable.version}</version>
</dependency>
<!-- JavaX Annotation API -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>provided</scope>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<!-- Spring Boot Data JPA starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot Validation starter with exclusions for validation-api and javax.annotation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<exclusions>
<exclusion>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</exclusion>
<exclusion>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Boot Webflux starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- ModelMapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>
<!-- SpringDoc OpenAPI starter for WebMVC UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- Maven build configuration including Spring Boot and OpenAPI generator plugins -->
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- OpenAPI Generator Maven Plugin -->
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.1.0</version>
<executions>
<execution>
<id>generate-api-code</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<!-- OpenAPI YAML specification file path -->
<inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
<!-- Various configuration options for code generation -->
<generateSupportingFiles>true</generateSupportingFiles>
<generatorName>spring</generatorName>
<strictSpec>true</strictSpec>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<!-- Controller exceptions, interface-only, skip default interface, and more -->
<configOptions>
<!-- Controller exceptions -->
<controllerThrowsExceptions>java.io.IOException,com.example.NotFoundException</controllerThrowsExceptions>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useBeanValidation>true</useBeanValidation>
<useClassLevelBeanValidation>false</useClassLevelBeanValidation>
<useTags>true</useTags>
<java17>true</java17>
<useOptional>false</useOptional>
<library>spring-boot</library>
<hideGenerationTimestamp>true</hideGenerationTimestamp>
<dateLibrary>java17</dateLibrary>
<bigDecimalAsString>true</bigDecimalAsString>
<useBeanValidation>true</useBeanValidation>
<apiPackage>${openapi.package.api}</apiPackage>
<modelPackage>${openapi.package.model}</modelPackage>
</configOptions>
<!-- Type mappings and import mappings -->
<typeMappings>
<typeMapping>OffsetDateTime=LocalDateTime</typeMapping>
</typeMappings>
<importMappings>
<importMapping>java.time.OffsetDateTime=java.time.LocalDateTime</importMapping>
</importMappings>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Your dependencies, as well as your configuration, are a mess of incompatible versions. Incompatible with each other and with Spring Boot.
PRO-TIP: As soon as you are starting to add excludes
to Spring Boot Starter, take your hands of the keyboard, rollback your chair and rethink. Because then you are doing it wrong!
First fix your dependencies.
<dependencies>
<!-- Spring Boot Web starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>compile</scope>
</dependency>
<!-- Swagger Annotations -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger.annotations.version}</version>
</dependency>
<!-- OpenAPI tools Jackson Databind Nullable -->
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>${jackson-nullable.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
Next instruct your OpenAPI generator to use JakartaEE instead of JavaEE. For this add the useJakartaEe
with value true
to the configOptions
of your openapi-generator-maven-plugin
.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.1.0</version>
<executions>
<execution>
<id>generate-api-code</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<!-- OpenAPI YAML specification file path -->
<inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
<!-- Various configuration options for code generation -->
<generateSupportingFiles>true</generateSupportingFiles>
<generatorName>spring</generatorName>
<strictSpec>true</strictSpec>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<!-- Controller exceptions, interface-only, skip default interface, and more -->
<configOptions>
<!-- Controller exceptions -->
<controllerThrowsExceptions>java.io.IOException,com.example.NotFoundException</controllerThrowsExceptions>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useBeanValidation>true</useBeanValidation>
<useClassLevelBeanValidation>false</useClassLevelBeanValidation>
<useTags>true</useTags>
<java17>true</java17>
<useOptional>false</useOptional>
<library>spring-boot</library>
<hideGenerationTimestamp>true</hideGenerationTimestamp>
<dateLibrary>java17</dateLibrary>
<bigDecimalAsString>true</bigDecimalAsString>
<useBeanValidation>true</useBeanValidation>
<apiPackage>${openapi.package.api}</apiPackage>
<modelPackage>${openapi.package.model}</modelPackage>
<useJakartaEe>true</useJakartaEe>
</configOptions>
<!-- Type mappings and import mappings -->
<typeMappings>
<typeMapping>OffsetDateTime=LocalDateTime</typeMapping>
</typeMappings>
<importMappings>
<importMapping>java.time.OffsetDateTime=java.time.LocalDateTime</importMapping>
</importMappings>
</configuration>
</execution>
</executions>
</plugin>
Finally remove the @Validated
from your controller class only leave the @Valid
on the method parameter. The @Validated
on the method serves a different purpose and can (and will) lead to some surprising results.