Bean validation - validate optional fields - spring

Given a class that represents payload submitted from a form, I want to apply bean validation to a field that may or may not be present, for example:
class FormData {
#Pattern(...)
#Size(...)
#Whatever(...)
private String optionalField;
...
}
If optionalField is not sent in the payload, I don't want to apply any of the validators above, but if it is sent, I want to apply all of them. How can it be done?
Thanks.

So usually all of these constraints consider null value as valid. If your optional filed is null when it's not part of the payload all should work just fine as it is.
And for any mandatory fields you can put #NotNull on them.
EDIT
here's an example:
class FormData {
#Pattern(regexp = "\\d+")
#Size(min = 3, max = 3)
private final String optionalField;
#Pattern(regexp = "[a-z]+")
#Size(min = 3, max = 3)
#NotNull
private final String mandatoryField;
}
#Test
public void test() {
Validator validator = getValidator();
// optonal field is null so no violations will rise on it
FormData data = new FormData( null, "abc" );
Set<ConstraintViolation<FormData>> violations = validator.validate( data );
assertThat( violations ).isEmpty();
// optional field is present but it should fail the pattern validation:
data = new FormData( "aaa", "abc" );
violations = validator.validate( data );
assertThat( violations ).containsOnlyViolations(
violationOf( Pattern.class ).withProperty( "optionalField" )
);
}
You can see that in the first case you don't get any violations as the optional field is null. but in the second exmaple you receive a violation of pattern constraint as aaa is not a string of digits.

Related

Assert multiple field error codes from Validation using MockMvc

I am trying to assert two errors due to two given constraints to my form. My form has two constraints on its single field:
#Data
#NoArgsConstructor
#NotExistingGroup(groups = SecondGroupValidation.class)
public class GroupForm {
#NotBlank(groups = FirstGroupValidation.class)
#Size(min = 2, max = 30, groups = FirstGroupValidation.class)
private String name;
}
With the following test, I want to trigger both the #NotBlank and #Size validation and assert both raised errors:
#Test
void givenGroupEmptyName_groupPost_assertErrors() throws Exception {
mvc.perform(post("/groups/add").param("name", ""))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("groups-add"))
.andExpect(model().hasErrors())
.andExpect(model().attributeErrorCount("groupForm", 2))
.andExpect(model().attributeHasFieldErrorCode("groupForm", "name", "NotBlank"))
.andExpect(model().attributeHasFieldErrorCode("groupForm", "name", "Size"));
}
The mvc doPrint() method shows both errors are given
ModelAndView:
View name = groups-add
View = null
Attribute = groupForm
value = GroupForm(name=)
errors = [Field error in object 'groupForm' on field 'name': rejected value []; codes [NotBlank.groupForm.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [groupForm.name,name]; arguments []; default message [name]]; default message [must not be blank], Field error in object 'groupForm' on field 'name': rejected value []; codes [Size.groupForm.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [groupForm.name,name]; arguments []; default message [name],30,2]; default message [size must be between 2 and 30]]
However, the test breaks with the following error
java.lang.AssertionError: Field error code expected:<Size> but was:<NotBlank>

InvalidPathException while sorting with org.springframework.data.domain.Pageable

I am trying to sort my table's content on the backend side, so I am sending org.springframework.data.domain.Pageable object to controller. It arrives correctly, but at the repository I am getting org.hibernate.hql.internal.ast.InvalidPathException. Somehow the field name I would use for sorting gets an org. package name infront of the filed name.
The Pageable object logged in the controller:
Page request [number: 0, size 10, sort: referenzNumber: DESC]
Exception in repository:
Invalid path: 'org.referenzNumber'","logger_name":"org.hibernate.hql.internal.ast.ErrorTracker","thread_name":"http-nio-8080-exec-2","level":"ERROR","level_value":40000,"stack_trace":"org.hibernate.hql.internal.ast.InvalidPathException: Invalid path: 'org.referenzNumber'\n\tat org.hibernate.hql.internal.ast.util.LiteralProcessor.lookupConstant(LiteralProcessor.java:111)
My controller endpoint:
#GetMapping(value = "/get-orders", params = { "page", "size" }, produces = { MediaType.APPLICATION_JSON_VALUE })
public ResponseEntity<PagedModel<KryptoOrder>> getOrders(
#ApiParam(name = "searchrequest", required = true) #Validated final OrderSearchRequest orderSearchRequest,
#PageableDefault(size = 500) final Pageable pageable, final BindingResult bindingResult,
final PagedResourcesAssembler<OrderVo> pagedResourcesAssembler) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().build();
}
PagedModel<Order> orderPage = PagedModel.empty();
try {
var orderVoPage = orderPort.processOrderSearch(resourceMapper.toOrderSearchRequestVo(orderSearchRequest), pageable);
orderPage = pagedResourcesAssembler.toModel(orderVoPage, orderAssembler);
} catch (MissingRequiredField m) {
log.warn(RESPONSE_MISSING_REQUIRED_FIELD, m);
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(orderPage);
}
the repository:
#Repository
public interface OrderRepository extends JpaRepository<Order, UUID> {
static final String SEARCH_ORDER = "SELECT o" //
+ " FROM Order o " //
+ " WHERE (cast(:partnerernumber as org.hibernate.type.IntegerType) is null or o.tradeBasis.account.retailpartner.partnerbank.partnerernumber = :partnerernumber)"
+ " and (cast(:accountnumber as org.hibernate.type.BigDecimalType) is null or o.tradeBasis.account.accountnumber = :accountnumber)"
+ " and (cast(:orderReference as org.hibernate.type.LongType) is null or o.tradeBasis.referenceNumber = :orderReference)"
+ " and (cast(:orderReferenceExtern as org.hibernate.type.StringType) is null or o.tradeBasis.kundenreferenceExternesFrontend = :orderReferenceExtern)"
+ " and (cast(:dateFrom as org.hibernate.type.DateType) is null or o.tradeBasis.timestamp > :dateFrom) "
+ " and (cast(:dateTo as org.hibernate.type.DateType) is null or o.tradeBasis.timestamp < :dateTo) ";
#Query(SEARCH_ORDER)
Page<Order> searchOrder(#Param("partnerernumber") Integer partnerernumber,
#Param("accountnumber") BigDecimal accountnumber, #Param("orderReference") Long orderReference,
#Param("orderReferenceExtern") String orderReferenceExtern, #Param("dateFrom") LocalDateTime dateFrom,
#Param("dateTo") LocalDateTime dateTo, Pageable pageable);
}
Update:
I removed the parameters from the sql query, and put them back one by one to see where it goes sideways. It seems as soon as the dates are involved the wierd "org." appears too.
Update 2:
If I change cast(:dateTo as org.hibernate.type.DateType) to cast(:dateFrom as date) then it appends the filed name with date. instead of org..
Thanks in advance for the help
My guess is, Spring Data is confused by the query you are using and can't properly append the order by clause to it. I would recommend you to use a Specification instead for your various filters. That will not only improve the performance of your queries because the database can better optimize queries, but will also make use of the JPA Criteria API behind the scenes, which requires no work from Spring Data to apply an order by specification.
Since your entity Order is named as the order by clause of HQL/SQL, my guess is that Spring Data tries to do something stupid with the string to determine the alias of the root entity.

Javax validation of generics in Springboot with Kotlin

I have a controller:
#PostMapping
fun create(
#RequestBody #Valid request: MyContainer<CreateRequest>,
): MyContainer<Dto> = service.create(request.objects)
with MyContainer and CreateRequest looking something like this:
class MyContainer<T>(
#field:Valid // also tried param
#field:NotEmpty(message = "The list of objects can not be null or empty")
var objects: List<#Valid T>? = listOf(),
)
class CreateRequest(
#field:NotNull(message = "Value can not be null")
var value: BigDecimal? = null,
)
In my tests, the "outer" validation works, that is I do get the expected error message if I send it { "objects": null } or { "objects": [] }. But I can not get it to validate the contents of the list. From what I understand in Java List<#Valid T> should work, but for whatever I can not get it to work in kotlin.
I figured I might need some kind of use-site target on #Valid in List<#Valid T>, but I can't find one that's applicable for this use case.
How can I get the validation to work for the list?
I managed to find a solution myself.
Apparently get: is the correct use-site target, not field: or param:. Furthermore the #Valid in List<#Valid T> was not necessary.
For reference, here's the working class (also changed it back to a data class as that doesn't seem to pose an issue).
class MyContainer<T>(
#get:Valid
#get:NotEmpty(message = "The list of objects can not be null or empty")
var objects: List<T>? = listOf(),
)
and the CreateRequest:
class CreateRequest(
#get:NotNull(message = "Value can not be null")
var value: BigDecimal? = null,
)
Changing to the get: use-site target was only necessary for #Valid, but I opted for using it everywhere for consistency and since it seems to be the one that works best.

How to get an array of objects with spring #RequestParam in ManyToMany relationship?

I have an entity Movies and an entity Genres in a relationship many to many.
#ManyToMany
#JoinTable(name = "movies_genres", joinColumns = #JoinColumn(name = "movies_id"), inverseJoinColumns = #JoinColumn(name = "genres_id"))
private Set<Genres> genresSet = new HashSet<>();
In a rest controller class, I want to do this:
#GetMapping("/search-movies")
public Iterable<Movies> search(
#RequestParam(value = "genresSet", required = false) Set<Genres> genresSet,
#RequestParam(value = "synopsis", required = false) String synopsis,
#RequestParam(value = "title", required = false) String title,
#RequestParam(value = "runtime", required = false) Integer runtime
)
I use axios on front-end to send params to the back-end and genresSet is array of objects, for example
[
{ id: 1, name: 'action'},
{ id: 2, name: 'crime'},
{ id: 3, name: 'comedy'}
]
I thought that Spring would automatically convert array of objects into set of genres, but it gives me null.
How to get values of genres in form of a set of values?
To recap, the user enters more than one genre, where each genre is represented as an object, so front-end sends array of genre objects and back-end needs to bind that array to the set of genres, where set of genres is a many to many property of Movie entity.
You can rethink your parameters and change Set<Genres> either to Set<Integer> or Set<String>. Because you need just ids or just names of genres to use them in search query, you don't need to pass the whole Genres object.
#GetMapping("/search-movies")
public Iterable<Movies> search(
#RequestParam(value = "genresIds", required = false) Set<Long> genresIds,
// ... other parameters without changes
would accept
"genresIds": [1, 2, 3]
or
#GetMapping("/search-movies")
public Iterable<Movies> search(
#RequestParam(value = "genresNames", required = false) Set<String> genresNames,
// ... other parameters without changes
would accept
"genresNames": ['action', 'crime', 'comedy']
Another way you could accomplish this is to create a wrapper class for the Set. For example:
public class GenresWrapper {
Set<Genres> genresSet;
// ... accessors
}
Then your controller method would look like this:
#GetMapping("/search-movies")
public Iterable<Movies> search(
#ModelAttribute GenresWrapper genres,
// ... other parameters
)
Or you could wrap all the request parameters in a single wrapper object.
This answer is based on another SO answer.

GroupSequence and ordered evaluation in JSR 303

In our application we have such a case:
Constraints should be evaluated in particular order. (cheap to expensive)
Constraints should not be evaluated after a violation per field.
All fields should be validated.
For first two, groupsequence is fitting very good. However for my 3rd requirement I could not find a way to solve.
public class AccountBean {
#CheepValidation
#ExpensiveValidation
#VeryExpensiveValidation
private String name;
#CheepValidation
#ExpensiveValidation
#VeryExpensiveValidation
private String surname
}
For example,
Let's say that, for name field VeryExpensiveValidationconstraint is violated and for surname field ExpensiveValidation constraint is violated.
For this case I should display:
For field name: Only VeryExpensiveValidation error message
For field surname: Only ExpensiveValidation error message
Note that for field surname we did not evaluate VeryExpensiveValidation constraint.
Is there a way to implement it with JSR 303?
Thanks
You can use groups and #GroupSequence, but it's a bit unwieldy.
public class AccountBean {
#CheapValidation(groups=Name1.class)
#ExpensiveValidation(groups=Name2.class)
#VeryExpensiveValidation(groups=Name3.class)
String name;
#CheapValidation(groups=Surname1.class)
#ExpensiveValidation(groups=Surname2.class)
#VeryExpensiveValidation(groups=Surname3.class)
String surname;
public interface Name1 {}
public interface Name2 {}
public interface Name3 {}
#GroupSequence({Name1.class, Name2.class, Name3.class})
public interface Name {}
public interface Surname1 {}
public interface Surname2 {}
public interface Surname3 {}
#GroupSequence({Surname1.class, Surname2.class, Surname3.class})
public interface Surname {}
}
Then validate with:
validator.validate(myAccountBean,
AccountBean.Name.class, AccountBean.Surname.class)
The key is to have two entirely independent group sequences.
Unfortunately, it seems you must explicitly list the groups for all the fields you want to validate. I wasn't able to get it working with a 'default' #GroupSequence. Can anyone improve on this?
I've implemented ordered validation with GroupSequence but, generally speaking, GroupSequence beans validation implementation is not transparent.
Meaning, untill first group is fully validated, you can not trigger the validation of the second group.
E.g.
I have 3 validated fields with custom validators. The idea is pretty straightforward: every field should be validated with the set of validators from top to bottom independently (descending cardinality).
#StringPropertyNotNullOrEmptyConstraint(message = "Group name is required", groups = {ValidationStep1.class})
private final StringProperty groupName;
#StringPropertyNotNullOrEmptyConstraint(message = "Group password is required", groups = {ValidationStep1.class})
#StringPropertyMatchConstraint(message = "The given password phrases do not match", dependentProperties = {"groupPasswordMatch"}, groups = {ValidationStep2.class})
private final StringProperty groupPassword;
#StringPropertyNotNullOrEmptyConstraint(message = "Group password match is required", groups = {ValidationStep1.class})
#StringPropertyMatchConstraint(message = "The given passwords phrases do not match", dependentProperties = {"groupPassword"}, groups = {ValidationStep2.class})
private final StringProperty groupPasswordMatch;
public interface ValidationStep1 {
}
public interface ValidationStep2 {
}
#GroupSequence({GroupDialogModel.class, ValidationStep1.class, ValidationStep2.class})
public interface GroupDialogModelValidationSequence {
}
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<GroupDialogModel>> constraintViolations = validator.validate(this, GroupDialogModelValidationSequence.class);
The caveat of this approach is that each field should go through ValidationStep1 first and only after each validation of step 1 succeeds it goes to step 2. For example, even if password fields are not empty, but contain different values, validation for them succeeds if group name field does not contain any value. And only after I enter some value to the group name, ValidationStep1 group succeeds and then it displays validation result of ValidationStep2 (passwords do not match).
Making each group for each field in every sequence is bad practice IMHO, but it seems like there is no other choice.
Any other solution is much appreciated.

Resources