It seems that deeply nested validation doesn't work on a very simple example (using quarkus & lombok) using javax validation:
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
#Data
public class AModel {
#NotNull
#Valid
private BModel bModel;
#Data
public static class BModel {
#NotNull
#Valid
private CModel cModel;
}
#Data
public static class CModel {
#NotNull
private int number;
}
}
AModel class validations work without problem.
BModel class validations work without problem.
CModel class validations are fully ignored (e.g. #NotNull for number).
Am I doing something wrong, or am I hitting some sort of bug / limitation (very highly doubt that)?
Thanks
Related
I have a rest request entity in spring boot controller define like this:
package com.dolphin.rpa.application.command.credential;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* #author xiaojiang.jiang
* #date 2022/05/10
**/
#Data
public class CredentialDeleteCommand {
#NotNull(message = "should not be null")
private List<Long> ids;
#Min(1)
#Max(2)
private Integer mode;
}
Now I want to make sure the ids should not be null or size eqauls 0. I have tried these way:
#NotNull(message = "should not be null")
#NotEmpty(message = "should not be null")
#NotBlank(message = "should not be null")
But could not work. I also read the official docs that did not mentioned valid the List. Is it possible to valid the List elemnts? This is my reqeust json:
{
"ids": [
],
"mode": 1
}
Just adding #Size(min=1) is enough.
#Data
public class CredentialDeleteCommand {
#NotNull(message = "should not be null")
#Size(min=1)
private List<Long> ids;
#Min(1)
#Max(2)
private Integer mode;
}
The error is probably where you call this object to be validated. It must certainly be on some method from an instance that belongs to spring. So it should be a bean of spring container.
Normally you will use this in some method of the controller, as the controller already belongs to spring container.
#RequestMapping(path="/some-path",method=RequestMethod.POST)
public ResponseEntity doSomething(#Valid #RequestBody CredentialDeleteCommand credentialDeleteCommand){
....
}
Keep in mind though that #Valid should be placed before #RequestBody otherwise it does not work. At least in previous spring versions that I have faced this issue.
How is this?
#Size(min=1)
private List<Long> ids;
#Size or #NotNull should do the job if this dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
is specified.
Also dont forget about #Valid annotation.
Another way for arrays is to init them in model, so even if it's not specified in requestbody, you will get it initialized anyway.
I've been trying for days to find a similar problem online and can't seem to find anything so I am asking my question here.
I have a controller:
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#Validated
#RestController
#RequestMapping("/data")
public class TheController {
private final TheService theService;
#Autowired
public TheController(TheService theService) {
this.theService = theService;
}
#PostMapping(path = "/data", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.TEXT_PLAIN_VALUE})
public ResponseEntity<String> saveData(#Valid #RequestBody Data data) {
subscriptionDataFeedService.sendData(data.getDataList());
return ResponseEntity.ok()
.body("Data successful.");
}
}
I have the request body class:
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class Data {
#NotEmpty(message = "Data list cannot be empty.")
#JsonProperty(value = "dataArray")
List<#Valid DataOne> dataList;
}
I have the DataOne class:
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class DataOne {
private #NotBlank String currency;
private #NotBlank String accountNumber;
private #NotBlank String finCode;
private String poNumber;
private #NotBlank String invoiceNumber;
private #NotNull Address billTo;
private #NotNull Address soldTo;
private #NotNull LocalDate invoiceDate;
private #NotBlank String billingPeriod;
private #NotNull LocalDate paymentDueDate;
private #NotNull BigDecimal amountDue;
#JsonProperty(value = "activitySummary")
private #NotNull List<#Valid ProductSummary> productSummaryList;
#JsonProperty(value = "accountSummary")
private #NotNull List<#Valid AccountSummary> accountSummaryList;
#JsonProperty(value = "transactions")
private #NotNull List<#Valid Transaction> transactionList;
private #NotNull PaymentByACH paymentByACH;
private #NotNull Address paymentByCheck;
private #NotNull CustomerServiceContact customerServiceContact;
}
And I will include the Address class:
import javax.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class Address {
private #NotBlank String name;
private #NotBlank String address1;
private String address2;
private #NotBlank String city;
private #NotBlank String state;
private #NotBlank String postalCode;
}
I omitted some of the other classes because they aren't needed for my question.
So the problem I am having is that the #Valid annotation is able to validate everything except for the nested classes inside DataOne that aren't a list. In other words, it cannot validate the fields inside Address, PaymentByACH, etc. However, it is able to validate that those objects are #NotNull but is unable to validate the fields inside those classes.
The #Valid is unable to validate the name, address 1, city, etc fields inside of Address. Whenever I add an #Valid tag in front of the Address field inside DataOne I get an HV000028: Unexpected exception during isValid call exception.
How can I validate the nested fields inside of the Address object or any of the nested objects?
TL;DR: The objects that are a list, such as List<#Valid Transaction> transactionList; does validate the fields inside of Transaction but the code does not validate the fields inside of Address.
Great question.
I think you're slightly misusing the #Valid annotation.
How can I validate the nested fields inside of the Address object or
any of the nested objects?
#Valid shouldn't be prefixed to fields you want to validate. That tool is used specifically for validating arguments in #Controller endpoint methods (and sometimes #Service methods). According to docs.spring.io:
"Spring MVC has the ability to automatically validate #Controller
inputs."
It offers the following example,
#Controller
public class MyController {
#RequestMapping("/foo", method=RequestMethod.POST)
public void processFoo(#Valid Foo foo) { /* ... */ }
}
The only reason you should use #Valid anywhere besides in the parameters of a controller (or service) method is to annotate complex types, like lists of objects (ie: DataOne: productSummaryList, accountSummaryList, transactionList). These docs have details for implementing your own validation policy if you'd like.
For your practical needs, you should probably only be using #Valid on controller level methods and the complex types for models referenced by that method. Then use field-level constraints to ensure you don't get things like negative age. For example:
#Data
...
public class Person {
...
#Positive
#Max(value = 117)
private int age;
...
}
Check out this list of constraints you can use from the spring docs. You're already using the #NotNull constraint, so this shouldn't be too foreign. You can validate emails, credit cards, dates, decimals, ranges, negative or positive values, and many other constraints.
I am creating a Todo app in Spring Boot and I need to create two tables: Task and Todo(Todo extends Task).
In Task table is a field called description and I would like to prevent that column to be created in Todo table.
How can I do it?
Task(parent):
package com.example.todo.model;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Task {
#Id
private long id;
private String name;
private String description;
}
Todo(child):
package com.example.todo.model;
import javax.persistence.Entity;
import javax.persistence.Transient;
#Entity
public class Todo extends Task {
private boolean isChecked;
}
I would suggest you clean up your design because concrete classes inheriting from other concrete classes is (often) a code smell. The proper solution to this is to factor out the common parts of both classes into a (abstract) super class and then add the specific fields to the concrete inheriting classes:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Completable {
#Id
private long id;
private String name;
}
#Entity
public class Task extends Completable {
private String description;
}
#Entity
public class Todo extends Completable {
private boolean isChecked;
}
so you have the behaviour grouped in the classes where it belongs and don't have to make sure that one thing contains a description while it shouldn't.
What you want cannot be done easily. But you might be trying to solve an issue in the wrong way.
From what I am reading you have a Task entity with has two separate types:
one with a checkbox indicating its completion
one with an additional description
If this is the case you might want to model the classes the same way. Thus having:
A Task entity without the description
A Todo entity extending Task with the checkbox
A new SummaryTask extending Task with a description field
Updated Spring boot to 2.2.X from 2.1.X and elastic search to 6.8.X from 6.3.X.
Got mapping exception, to resolve Mapping exception, renamed document variable to myDocument.
Now on elasticSearchRepo.SaveAll(objectTosave) value is not persisted in document.
Other properties like id, category are present in the document.
Is there any way to have different fieldName and jsonProperty?
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
#NoArgsConstructor
#Data
#EqualsAndHashCode
#ToString
#Document(indexName="my_document_index", type="information", createIndex=false)
#JsonIgnoreProperties(ignoreUnKnown = true)
#JsonInclude(Include.NON_NULL)
public class MyInstance
{
#Field
private String id;
#Field
private String category;
#Field
#JsonProperty("document")
private MyObject mydocument;
/** JSON Creator **/
#JsonCreator
public MyInstance(#JsonProperty("id") id, #JsonProperty("category") category,
#JsonProperty("document") mydocument)
{
this.id = id;
this.category = category;
this.mydocument = mydocument;
}
}
No need to annotate the id property with #Field, you should rather put #Id there. Although this is not needed, as the name of the property is enough, it makes it clearer what it is.
As for the mydocument property not being persisted: It is but in Elasticsearch with the name mydocument. The #JsonProperty("document") defines the name of this property in JSON when mapped by Jackson, when you get this in over a REST endpoint for example. Renaming to mydocument inhibits the error that the property is interpreted as id property.
But I think you want to have as document in Elasticsearch as well. You can define the name of a property in Elasticsearch by setting it in the #Field annotation:
#Document(indexName="my_document_index", createIndex=false)
public class MyInstance
{
#Id
private String id;
#Field
private String category;
#Field(name = "document")
#JsonProperty("document")
private MyObject mydocument;
}
I am working on converting a jpa entity to use lombok. The resulting code is the following:
#Entity
#Table(name = "TEST")
#Data
#NoArgsConstructor
#AllArgsConstructor
class Test {
...
#Column(name = "FORMATTING")
#Enumerated(EnumType.ORDINAL)
private FormatType formatType;
...
}
The resulting error message contains the following
Caused by: org.hibernate.HibernateException: Missing column: formatType in TEST
I am really not sure what to google here. (I tried pasting everything before formatType into google - didn't see anything)
NOTE:
fields have been renamed and aspects which do not appear relevant have been omitted, for the sake of brevity and privacy. if something looks like a typo, it probably is. please let me know if you notice something so that i can address it.
the 3 lines describing the field are unchanged from the code i'm working with
EDIT:
I just noticed this right before the error message
13:22:19,967 INFO [org.hibernate.tool.hbm2ddl.TableMetadata] (ServerService Thread Pool -- 57) HHH000261: Table found: TABLE
13:22:19,967 INFO [org.hibernate.tool.hbm2ddl.TableMetadata] (ServerService Thread Pool -- 57) HHH000037: Columns: [..., formatType, ...]
13:22:19,968 ERROR [org.jboss.msc.service.fail] (ServerService Thread Pool -- 57) MSC000001: Failed to start service jboss.persistenceunit."...": org.jboss.msc.service.StartException in service jboss.persistenceunit."...": javax.persistence.PersistenceException: [PersistenceUnit: ...] Unable to build EntityManagerFactory
Should be functional
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "PARENT")
public abstract class Parent implements Serializable {
private static final long serialVersionUID = 1;
#Id
#Column(name = "ID")
#GeneratedValue
private long id;
#Column(name = "ENABLED")
private boolean enabled;
}
#Entity
#Table(name = "CHILD")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Child extends Parent {
/** XXX: HERE BE DRAGONS */
#Column(name = "ENUM_1")
#Enumerated(EnumType.STRING)
private Enum1 enum1;
#Column(name = "ENUM_2")
#Enumerated(EnumType.ORDINAL)
private Enum2 enum2;
/** XXX: NO MORE DRAGONS */
#Column(name = "FREQUENCY")
private String frequency;
#Column(name = "EMPTY")
private boolean empty;
#Column(name = "MAX_SIZE")
private int maxSize;
}
public enum Enum1 {
A,
B,
C
}
public enum Enum2 {
X,
Y,
Z
}
I have rolled back the lombok changes, I would still like to know what the issue is, but there is no rush. Also, thanks to this lovely little bug i am about 4 hours behind so i may be a little slow on the responses.
The pk of the child table is an fk to the parent table, and without lombok everything appears to work, despite the fact that the Child class has no id.
SOLUTION:
I completely forgot about asking this. Not long ago I revizited this problem. To explain the solution lets look at a slightly simplified version of the first example i included.
#Entity
#Table(name = "TEST")
#Setter
#Getter
class Test {
...
#Column(name = "FORMATTING")
#Enumerated(EnumType.ORDINAL)
private FormatType formatType;
...
}
It would appear that Lombok will give you this:
#Entity
#Table(name = "TEST")
class Test {
...
#Column(name = "FORMATTING")
#Enumerated(EnumType.ORDINAL)
private FormatType formatType;
public FormatType getFormatType() {
return formatType;
}
public void setFormatType(FormatType formatType) {
this.formatType = formatType;
}
...
}
Note that the annotations are still attached to the field. Now, I am not certain if it is just the version or implementation of JPA that we are using but I gather that if an accessor is defined jpa just ignores any annotations besides #Column (as well as any parameters specified for #Column - which is why jpa was looking for the wrong column name). So we actually need:
#Entity
#Table(name = "TEST")
class Test {
...
private FormatType formatType;
#Column(name = "FORMATTING")
#Enumerated(EnumType.ORDINAL)
public FormatType getFormatType() {
return formatType;
}
public void setFormatType(FormatType formatType) {
this.formatType = formatType;
}
...
}
After a great deal of confusion trying to find examples and fill in some specifics regarding how lombok does its thing (to be fair I am very easily confused) i discovered this little gem: onMethod=#__({#AnnotationsHere}). Utilizing this feature I came up with the following:
#Entity
#Table(name = "TEST")
#Setter
class Test {
...
#Getter(onMethod=#__({
#Column(name = "FORMATTING"),
#Enumerated(EnumType.ORDINAL)
}))
private FormatType formatType;
...
}
And presto it works. Now that we have what is apparently the only available solution I would like to address the question we are all pondering at the moment: is that really any cleaner than just writing the method manually and attaching the annotations there? Answer: ... I have no idea. I am just happy I found a solution.
Its strange. Can you show more code?
I'm trying to write a simple project with part of code like in your question and it worked. I used Spring Boot and MySQL. Try to check your configuration. There is my code:
Enum:
public enum FormatType {
FIRST_TYPE, SECOND_TYPE
}
Table in MySQL:
create table TEST
(
ID int auto_increment primary key,
FORMATTING int not null
);
Entity:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
#Entity
#Table(name = "TEST")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Test {
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column(name = "FORMATTING")
#Enumerated(EnumType.ORDINAL)
private FormatType formatType;
}
Repository:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface TestRepository extends JpaRepository<Test, Integer> {
}
Service:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
#Service
public class TestService {
private TestRepository repository;
#Autowired
public TestService(TestRepository repository) {
this.repository = repository;
}
public List<Test> getAllTestEntities() {
return repository.findAll();
}
}
Is unlikely that lombok causes runtime problems, as it works on precompile time, you might find useful to decompile the generated code, I sometimes find that the order in which lombok annotations are placed in the source code affect the final result, so, you use #Data and #NoArgsConstructor , I guess you can remove #NoArgsConstructor an try to see if that solves your problem.
I faced the same problem with Lombok and JPA but I setup the Lombok and it worked as expected. Below is the code:
Controller
package com.sms.controller;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.sms.model.StudentModel;
import com.sms.persistance.StudentRepository;
#RestController
public class StudentController {
#Autowired
private StudentRepository sr;
#PostMapping("/addstudent")
public String addStudent(#Valid #RequestBody StudentModel studentModel) {
StudentModel result = sr.save(studentModel);
return result.equals(null)?"Failed":"Successfully Saved student data";
}
}
Model
package com.sms.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
import lombok.RequiredArgsConstructor;
#Data
#RequiredArgsConstructor
#Entity
#Table(name="student", schema="development")
public class StudentModel {
#Id
#Column(name="student_id")
private int id;
#Column(name="student_name")
private String studentname;
#Column(name="student_address")
private String studentaddress;
}
Repository
package com.sms.persistance;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.sms.model.StudentModel;
#Repository
public interface StudentRepository extends JpaRepository<StudentModel, Integer>{
}