NotNull constraints aren't being enforced when not set - validation

NotNull constraints aren't being enforced, when the attribute is not set. Is there anything which needs to be additionally set to enforce null values
import javax.validation.constraints.NotNull;
#NotNull(message = "PaymentType is required")
#JsonProperty(PAYMENT_TYPE)
#Field(type = FieldType.Keyword, name = PAYMENT_TYPE)
private PaymentType paymentType;

As you are having a #JsonProperty(PAYMENT_TYPE) annotation on that property I assume you are using this entity not only to store data in Elasticsearch, but also are sending it to and receiving from some client application? Then the validation should happen when you get this data in. Spring Data Elasticsearch does not support validation on properties.

Related

Getting JSON parse error on JSON property createdAt when not included in request body

I am recently learning to build REST APIs with Kotlin and Spring Boot. I was trying to create my first domain model and controller end-point. Here is the implementation of the Country model and CountryController countroller.
/model/Country.kt
package com.example.example.model
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EntityListeners
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.sql.Timestamp
#Entity
#Table(name = "country")
class Country (
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "country_id")
val id: Int,
val code: String,
val displayName: String,
#CreatedDate
#Column(name = "created_at", nullable = false, updatable = false)
val createdAt: Timestamp,
#LastModifiedDate
#Column(name = "updated_at", nullable = false)
val updatedAt: Timestamp
)
/controller/CountryCountroller.kt
package com.example.example.controller
import com.example.example.model.Country
import com.example.example.repository.CountryRepository
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping
#RestController
class CountryController {
#Autowired
lateinit var countryRepository: CountryRepository
#PostMapping("/countries", consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE))
fun createCountry(#Valid #RequestBody country: Country): Country {
return countryRepository.save(country);
}
}
Schema of the Country table:
country_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(3) NOT NULL,
displayName VARCHAR(40) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (country_id)
When I started running the Spring Boot application and use Postman to call the POST /countries API, I received a 400 Bad Request error. The application error log says:
2023-01-12T14:09:15.423Z WARN 90037 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class com.example.example.model.Country] value failed for JSON property createdAt due to missing (therefore NULL) value for creator parameter createdAt which is a non-nullable type]
The request body is a json and the request header has include the correct Content-Type information:
{
"code": "FRA",
"displayName": "France"
}
I suspected that the Spring Boot did not ignore the two fields createdAt and updatedAt, therefore I have tried to add #JsonIgnore above createdAt and updatedAt. Unfortunately the same 400 Bad Request error was still returned.
Do I actually have to include createdAt and updatedAt in the JSON request body? However, what I wanted to achieve is that the createdAt and updatedAt should be added/updated either by the Spring Boot application putting the createdAt and updatedAt data when calling the repository methods or by MySQL DB when the query is being run. Is there any way to do so?
You are mixing up two concepts here
whether on not createdAt and updatedAt are allowed be null in your data model
serialization:
whether you want the RestController to deserialize the fields when you receive them in the POST
whether you want the RestController to serialize the fields when you implement a GET request
By using #JsonIgnore (or the Kotlin variation #set:JsonIgnore you are just avoiding the data deserialization but still the data model requires a value. The JPA annotations are going to get executed when you put the data in the database, but in the time that the object is created by the MVC infrastructure, and before the database save happens, the data does not match the model contract.
There are two solutions to what you want to do:
Relax the data model
Change your data model to allow nulls in the two fields:
#CreatedDate
#Column(name = "created_at", nullable = false, updatable = false)
val createdAt: Timestamp?,
#LastModifiedDate
#Column(name = "updated_at", nullable = false)
val updatedAt: Timestamp?
BTW I would expect you to get the error on id as well, perhaps an Exception would be given for that once you dealt with the two dates, because again, your POST data does not supply id and this not nullable.
Use a Request object
Create a simpler data model that is used to receive your POSTed data and then create the Domain/Database model object from it.
I use a mix of the two techniques depending on circumstances. The latter approach has benefit, e.g. when you have large objects which may only be partially sent by the client (think PATCH).

Spring boot using placeholders in messages.properties

I'm working with Spring Boot 2.1.5, and I'm using javax annotations to validate my class properties. So for example, I have:
class MyClass {
#NotEmpty(message = "{validation.notEmpty}")
private String company;
}
Then I have a messages.properties file that contains:
validation.notEmpty={0} is missing
I was hoping to get the error message "company is missing", but instead I'm getting "{0} is missing". What is the proper way of passing my variable name as placeholder?
Thanks.
You cannot achieve such a thing because #NotEmpty resolves the placeholder and get that value as message, it doesn't pass it to the placeholder to be added as parameter.
Look at the default message :
javax.validation.constraints.NotEmpty.message = must not be empty
You can just pass a string as value.
Validation messages are not default not designed to hold the field name. That is retrievable from the ConstraintViolation objects that provide (among other things) paths for each validation error.
So to achieve your requirement, what you could do is creating a custom annotation/validator with a second attribute :
#NotEmpty(message = "{validation.notEmpty}", field="company")
private String company;
But doing it for every kind of constraints you use looks an overhead.
Maybe just accept the duplication that is simple to refactor :
#NotEmpty(message = "car is missing")
private String company;
Note that you loses the benefit from locales, which may be undesirable if you handle multiple languages.
In this case, a more robust approach would be to use ConstraintViolations that has the path value of the field.
If my view doesn't address your issue, I encourage to post the question in the Hibernate Validator issues tracker.

Spring Data MongoDB document expiry time

I've set up a simple document model (below) along with a Spring Data repository
#Document(collection = "users")
public class UserDocument {
#Id
private String userId;
#Indexed(expireAfterSeconds=3600)
private LocalDateTime registeredDate;
}
This seems to work fine and deletes the user documents after whatever time I set expireAfterSeconds to. However, rather than typing in a value of 3600 I'd like to pull that number from a config .yml. The usual way of adding #Value("${config.file.path.of.expiry}") won't work because #Indexedrequires the value to be a runtime constant, which #Value isn't.
Is there any other way to set up document expiry dates in Spring/Mongodb that doesn't use #Indexed(expireAfterSeconds=x)?
Instead of using the Indexed annotation to declare an index which Spring Data Mongo will create on your behalf ... you could create the index explicitly using a MongoTemplate instance.
#Value("${config.file.path.of.expiry}")
long expireAfterSeconds;
...
mongoTemplate.indexOps("users").ensureIndex(
new Index().on("registeredDate", Order.ASCENDING)
.expire(expireAfterSeconds)
);
This would allow you to source the value for expireAfterSeconds from a configuration file at runtime rather than hardcoding it in an annotation.

JPA - Auto-generated field null after save

I have an Account entity and I'm trying to persist it using save function. My code:
#Override
public Account createAccount(String pin) {
Account account = new Account();
account.setBalance(0L);
account.setPin(pin);
return accountRepository.save(account);
}
Now my entity class has an autogenerated field called accountNumber. My entity class:
#Entity
#Table(name = "accounts")
#Data
public class Account {
#Column(name = "account_number", length = 32, insertable = false)
private String accountNumber;
private Long balance;
}
Now after calling save, the entity returned has accountNumber as null but i can see in the intellij database view that it is actually not null. All the other auto-generated fields like id etc are there in the returned entity just the accountNumber is null. Default value for accountNumber is set in the sql file :
ALTER TABLE accounts
ALTER COLUMN account_number SET DEFAULT DefaultValueSerializer(TRUE, TRUE, 12);
Here, DefaultValueSerializer is the function which is generating the account number.
I've tried other solutions available here like using saveAndFlush() etc, nothing worked in my case. What can be an issue?
As mentioned in comment Hibernate is not aware about what happens in database engine level so it does not see the value generated.
It would be wise to move generation of account number to JPA level instead of using db defaults.
I suggest you to study annotations #GeneratedValue and related stuff like #SequenceGenerator. That way the control of generating account number is in JPA level and there is no need for stuff like refreshing entity after save.
One starting point: Java - JPA - Generators - #SequenceGenerator
For non-id fields it is possible to generate value in method annotated with #PrePersist as other answer suggests but you could do the initialization already in the Accounts constructor .
Also see this answer for options.
You can create an annotated #PrePersist method inside the entity in which you set its fields to their default value.
That way jpa is going to be aware of the default.
There are other such annotation avaiable for different entity lifecycle event https://docs.jboss.org/hibernate/entitymanager/3.6/reference/en/html/listeners.html
P.s. if you decide to go this way remember to remove the insertable = false
Use
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
for your IDs. And also leave your saving to saveAndFlush so you can immediately see the changes, If any. I'd also recommend separating IDs and account numbers. They should not be the same. Try debugging your program and see where the value stops passing around.

Ignoring spring mvc JSR-303 validations for selective entity fields

I have spring4 mvc application to save an Address entity, code bit as follows.
My Controller
#RequestMapping(value = "addAddress", method = POST)
public String registerComplaint(#Valid #ModelAttribute final Address address, final BindingResult resultBinder) {
if (resultBinder.hasErrors())
return "addAddress";
addressService.addAddress(address);
return "redirect:myAddress";
}
My Entity
#Entity
#Table(name = "address")
public class Address {
#NotNull
private String street;
#NotNull
private String pin;
#NotNull
private String createdBy;
........
}
My form conatins only street and pin as field, where as createdBy should be set by me after validating the other form values.
Here the problem is spring JSR303 validation support is validating a field ie createdBy which i don't want to validate by spring mvc.
How can i instruct spring mvc not to validate these kind of optional fields while using #Valid annotation.
Is there any way i can skip fields like this using spring mvc ?
Validation is mainly for user input. Since you will be setting createdBy yourself, just do so before saving it (e.g #PrePersist), or have a new Date as a default value. If you need to enforce a constraint for createBy, you can do so at the schema level.
#Column(nullable=false, ...)
private String createdBy = new Date();
You need to read up on Validation Groups. This lets you use different validators depending on the "scenario"
Use Spring's #Validated annotation to use groups
If you don't protect the createdBy field, a user can change it by altering the POST variables. See DataBinder.setDisallowedFields()
Conceptually, how is a pin related to an address?
It sounds like you want to use a Form Backing Object here (a regular non-JPA POJO made just for a form), and copy values to your real entities.

Resources