Skip to content

Instantly share code, notes, and snippets.

@Ekatereana
Created July 25, 2023 11:43
Show Gist options
  • Save Ekatereana/89c1a4dff1b753d8f059ff16a57e461c to your computer and use it in GitHub Desktop.
Save Ekatereana/89c1a4dff1b753d8f059ff16a57e461c to your computer and use it in GitHub Desktop.
# Pretty little Jakarta 3.0 magic U will want to use
Hi there, today we will talk about request validation with Jakarta 3.0 and all the excellent magic tricks you could benefit from. Of course, we all know about @NotNull and @NotEmpty. However, Jakarta's capabilities are far away beyond that. This article aims to check out my most beloved of them with small true-life samples.
## Why it’s cool to have validation separated from service logic?
In a few words: it will simplify your life and secure your service layer from leaking validation logic. Also, having a single source of truth for all validation errors that may and will occur is always nice. And the cherry on top — boxing validation exceptions into the Jakarta types allows you to handle all these exceptions centrally with @ControllerAdvice.
Three reasons packed up — pretty lovely things for the developer’s heart.
## Fabulous set of annotations for constraint validation
Let’s start with the built-in annotations you may use out of the box. There are plenty of them for validation of:
- data-time fields (@Future, @Past, @FutureOrPresent, @PastOrPresent);
- strings (@Email, @Pattern, @Size, @NotEmpty);
- iterative objects (@Size, @NotBlank)
- numeric values (@Digits, @Min and @Max, @Negative, @Positive)
- boolean values (@AssertTrue, @AssertFalse)
- objects (@Null, @NotNull, @Valid)
And most of them you are probably already familiar with. However, the genuine fun begins with these annotations' composition and advanced targeting.
First, you need to know that each build-in annotation (aka `Constraint`) has a list of targets to which it could be applied. In Jakarta EE, these targets are defined as `ElementTypes`. Available variants of `ElementType` are:
- `FIELD` for constrained attributes
- `CONSTRUCTOR` for constrained constructor return values
- `METHOD` for constrained getters and constrained method return values
- `PARAMETER` for constrained method and constructor parameters
- `TYPE` for constrained beans
- `ANNOTATION_TYPE` for constraints composing other constraints
- `TYPE_USE` for container element constraints
- `METHOD`
- `CONSTRUCTOR`
- `ANNOTATION_TYPE` for cross-parameter constraints composing other cross-parameter constraints
So what? You may ask, but it’s where the magic begins — all build-in Jakarta constraints could be applied not only to field or method parameters but to the method or constructor itself
Let’s check out how we may use this knowledge by example. Imagine we have such a use case:
> We receive the request containing a set of available color themes for an e-shop and defaultColorTheme, which would be displayed automatically. Business logic requires us to validate it for the condition that the set of available color themes contain default one.
>
It smells like validation leaked in the controller layer, isn’t it?
Actually nope. And it could be solved with **@AssertTrue** annotation.
```java
@JsonProperty("default")
String defaultTheme;
List<String> available;
// marking this field as ignorant for the serializer and deserializer
@JsonIgnore
@AssertTrue(message = "Available themes must contain default theme")
public boolean isAvailableContainsDefault() {
return available.contains(defaultTheme);
}
```
In the example above, we target @AssertTrue over the method and use @JsonIgnore annotation to hide the introduced field from serialization.
<aside>
💡 Please be mindful of naming for the validation method and accessibility level. The method should be public and start with the prefix “is”, or Jakarta will ignore it.
</aside>
Excellent!
Similarly, we may validate method output as a @Positive integer, ensure that the result date is in the @Future, or control the number of decimal places in the method result using @Digits.
You may play around with other options — check out a complete list of build-in constraints and their properties description in the [official documentation](https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html#builtinconstraints).
The next step for our magic tour is more advanced tricks, and let’s get strict with the business.
## Write custom annotations for even more lovely things to do
The built-in constraints are awesome and nice to use. However, the business often has more complex requirements than out-of-the-box functionality may cover. For such cases, you may define your own Constraint annotations and Validators.
And from this point, we need to dig underhood of the Jakarta EE processes (yay)
### Few words on Jakarta Flow
As we know, Jakarta operates with constraints — Java Beans that are marked with the `@Constraint`annotation and implement Jakarta Validation API obligatory methods:
```java
// defines Jakarta groups that constraint applies to
Class<?>[] groups() default {};
// defines an array of Payload obj associated with the constraint
// defaults to []
Class<? extends Payload>[] payload() default {};
// default message that would be thrown in ConstraintValidationException
String message() default "Error message";
```
The Constraint interface defines the method validatedBy() that would be used to inject the corresponding validator and defines the target `ElementTypes` for annotation to apply.
```
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
Class<?extends ConstraintValidator<?, ?>>[] validatedBy();
}
```
To create your annotation, register it via @Constraint and provide the Validator implementation Jakarta would use to check corresponding targets (methods, fields, parameters, etc.)
### Defining annotation
Let’s see how it works with the example:
> Imagine we have usecase when we store a e-shop configuration per country. Requirement suggest us to implement delete operation for these configuration by country code, howver we need to validate if the input code is actually in the list of ISO countries set.
>
So what we gonna do is to define @ValidCountryCode annotation that checks the path parameter from the REST request to be one of the ISO country codes.
The first step is to define the annotation interface. We will validate the request parameter so that the target element type is PARAMETER (such a surprise); I will also add the custom message for the validation error; other methods I will leave as default; we don’t need them for now.
```java
@Target({ElementType.PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = CountryValidator.class)
@Documented
public @interface ValidCountryCode {
String COUNTRY_CODE_IS_NOT_VALID = "Country code is not valid";
String message() default COUNTRY_CODE_IS_NOT_VALID;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
```
To the @Constraint constructor, I will pass the custom validator class.
According to Jakarta spec, Validators for Constraints must implement methods of `ConstraintValidator<A extends Annotation, T>`interface — boolean isValid(). Where A is our future annotation, and T is the type of target to validate. In our case — ValidCountryCode and String. We will add simple logic to check `whether` the input string belongs to ISO counties.
```java
@Component
public class CountryValidator implements ConstraintValidator<ValidCountryCode, String> {
private static final COUNTRIES = Locale.getISOCountries();
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return!COUNTRIES.stream().noneMatch(element -> element.equals(value));
}
}
```
And … that’s it! So easy and effective now we have a custom annotation to use:
```java
@RestController
@Validated
@RequestMapping(value = "v1/countries")
public class MarketConfigurationsController {
...
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping(value = "/:country/", produces = {MediaType.APPLICATION_JSON_VALUE})
public void deleteConfiguration(@PathVariable("country") @ValidCountryCode String country) {
countryService.deleteCountryByCode(country);
}
...
}
```
### Few words about Jakarta Exception handling
As was mentioned above, one of Jakarta's Validation benefits is the validation exception boxing,
## Validation groups
Groups defined by Jakarta EE documentation as :
> A group defines a subset of constraints. Instead of validating all constraints for a given object graph, only a subset is validated. This subset is defined by the group or groups targeted. Each constraint declaration defines the list of groups it belongs to. If no group is explicitly declared, a constraint belongs to the `Default` group.
>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment