Pollito Dev
January 8, 2024

Contract-Driven Development 5: Controller validations aren't working... Why?

Posted on January 8, 2024  •  9 minutes  • 1849 words  • Other languages:  Español

Workaround obsolete javax validations in Spring Boot 3.

Check the github repo

This is a continuation of Contract-Driven Development 4: Generating controller interfaces .

Everything we’ll do here, you can find in in the github repo.

Spring City Explorer - Backend: Branch feature/cdd-5

Changes to the openAPI Specification.yaml

{
  "timestamp": {
    "offset": {
      "totalSeconds": 0,
      "id": "Z",
      "rules": {
        "fixedOffset": true,
        "transitions": [],
        "transitionRules": []
      }
    },
    "year": 2024,
    "monthValue": 1,
    "dayOfMonth": 3,
    "hour": 18,
    "minute": 31,
    "second": 48,
    "nano": 218000000,
    "dayOfWeek": "WEDNESDAY",
    "dayOfYear": 3,
    "month": "JANUARY"
  }
}

This can cause serialization issues in whoever consumes our service.

Lets write some unit tests for our controllers

Here is an example of how to test ArticleController

@ExtendWith(MockitoExtension.class)
class ArticleControllerTest {
  @InjectMocks private ArticleController articleController;
  @Mock private ArticleService articleService;

  @Test
  void whenGetArticlesByCountryThenReturnsArticles() {
    ResponseEntity<Articles> expectedResponse = ResponseEntity.ok(mockArticles());
    when(articleService.getArticlesByCountry(anyString(), anyInt(), anyInt()))
        .thenReturn(expectedResponse.getBody());

    ResponseEntity<Articles> actualResponse =
        articleController.getArticlesByCountry(MOCK_STRING, 0, 0);

    assertEquals(expectedResponse.getBody(), actualResponse.getBody());
  }
}

For this to work, is necesary to:

public interface ArticleService {
  Articles getArticlesByCountry(String country, Integer limit, Integer offset);
}
@Service
public class ArticleServiceImpl implements ArticleService {
  @Override
  public Articles getArticlesByCountry(String country, Integer limit, Integer offset) {
    return null;
  }
}
@RestController
@RequiredArgsConstructor
public class ArticleController implements ArticleApi {

  private final ArticleService articleService;

  @Override
  public ResponseEntity<Articles> getArticlesByCountry(
      String country, Integer limit, Integer offset) {
    return ResponseEntity.ok(articleService.getArticlesByCountry(country, limit, offset));
  }
}

Great, let’s write a failing test… Why isn’t failing?

Let’s quickly change a line in ArticleControllerTest

ResponseEntity<Articles> actualResponse =
        articleController.getArticlesByCountry(MOCK_STRING, 100, 0);

In our specification, we stated that limit has a maximum of 10, so for sure 100 should throw an exception right?… Test passed.

Well for sure this is some Mockito stuff not mocking correctly. Let’s just run the application and cURL it.

curl --location 'http://localhost:8080/article?limit=101'

Got 200 OK. So, who’s to blame? Short answer, the plugin, cause it’s outdated for current standards. Long answer and how to workaround it, keep reading.

Little bit of background: javax, jakarta, and Spring Boot 3

I asked chatGPT:

explain without much technical details whats the deal with the javax and jakarta packages, focusing on what the libraries do and why moving from javax to jakarta

And I got this:

javax Packages

Transition to Jakarta

Jakarta Packages

Spring Boot 3 drops javax in favor of jakarta

This change was driven by the move of Java EE to Jakarta EE under the Eclipse Foundation, which led to the renaming of packages from javax to jakarta.

For Spring Boot 3, here are the key points regarding compatibility with javax packages:

What does that has to do with the controller validations don’t working then?

Well, sadly the plugin is only able to generate code in the pre Spring Boot 3 way, using javax. We can check that going into the interface our controller extends and reading into the imports. We will find:

import javax.validation.Valid;
import javax.validation.constraints.*;

So what is happening is that our Spring Boot 3 application is simply ignoring the javax validation, resulting in our current behaviour.

So what are our options?

Pros and cons of the chosen hack fix

Pro: Now that we are writing our own validations, we can even improve on things that the OAS falls short

While the OAS provides a robust framework for standard API validations, it can sometimes fall short in handling complex or unique validation scenarios that are specific to certain business logic or data formats. By writing our own validations, we can introduce a level of specificity and flexibility that the OAS might not inherently support.

This approach allows for a more granular control over the data integrity and the behavior of the API, ensuring that it aligns more precisely with the application’s requirements and user expectations. Furthermore, custom validations can also serve as a means to introduce additional security checks or to enforce certain best practices that are beyond the scope of the OAS, thereby enhancing the overall robustness and reliability of the API.

Pro: Is the least disruptive for the current situtation

The choice of implementing custom validations often emerges as a highly efficient and minimally disruptive solution, especially when compared to more drastic measures such as altering existing libraries.

Con: We are doing manual work that is prone to fall in obsolence

Imagine that the requirements changes, and your architect or you create yourServiceOAS_V2.yaml. Now is not only drop, build and test. Now you have to manually implement changes.

Doing things manually implies that there’s a chance of missing something. We developers can and will make mistakes. If something can be automatized to prevent avoidable human mistakes, it is good to put some thinnk effort into it.

Con: The further away the error occurs, the more difficult it is to map its HTTP response status state

Giving the service the responsability of validate request inputs does not follow the “Early input validation principle”. If an error is in the request, should be thrown as soon as possible. In this case, that should be in the controller.

Also this rises a new problem: now that the error is in the service, what status do I map it to? One would think “easy 400-ish”. But how can you be so certain?

I think this is the biggest con of all. I’ll let it slide at the moment, but the ideal solution is to not have a problem to begin with, so we are gonna be looking for a better plugin in the future.

Implementing the solution

Add jakarta in pom.xml

<!--
  It integrates the Hibernate Validator and the Validation API, providing a seamless experience for adding validation capabilities to Spring Boot applications.
  The specified version ensures compatibility with other Spring Boot 3.x components
-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
  <version>3.1.2</version>
  <scope>compile</scope>
</dependency>

Add jakarta annotations to the service interface

import dev.pollito.springcityexplorer.annotation.ValidArticleCountry;
import dev.pollito.springcityexplorer.models.Articles;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;

@Validated
public interface ArticleService {
  Articles getArticlesByCountry(
      @ValidArticleCountry String country,
      @Min(1) @Max(10) Integer limit,
      @Min(0) @Max(10000) Integer offset);
}

Notice that:

Run and see it working

Request

curl --location 'http://localhost:8080/article?country=asd'

Response: at the moment is mapping to 500, as any ConstraintViolationException in a service would. Won’t worry much about this right now.

{
  "timestamp": "2024-01-09T12:34:31.755+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "trace": "all the trace exception long long text",
  "message": "getArticlesByCountry.country: Invalid country code",
  "path": "/article"
}

Dance and repeat for the rest of the endpoints. In POST /comment, it has a class as request body. You will need to replicate that class in your src code with jakarta annotations.

Other minor changes in pom xml

Next steps

Hey, check me out!

You can find me here