Kotlin spring boot #RequestBody validation is not triggered - spring

I have a problem on a project with validating #RequestBody by using
implementation("org.springframework.boot:spring-boot-starter-validation")
My DTO looks like this:
import javax.validation.constraints.Email
import javax.validation.constraints.Pattern
class LoginDto(
#Email
val email: String,
#Pattern(regexp = Constants.PASSWORD_REGEX)
val password: String
)
And Controller looks like this:
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.RestController
import javax.validation.Valid
#RestController
#Validated
class AuthController(private val authService: AuthService) {
#PostMapping("login")
fun login(#Valid #RequestBody loginDto: LoginDto): LoginResponse {
return authService.login(loginDto)
}
...
}
And there is no error from validation, if I try to pass invalid data:
{
"password":"hello",
"email":"dfdfdfdf"
}
I get no error
I use Exposed instead of jpa but I think it's not related to the problem

You should change the annotations of #email and #Pattern to #field:Email and #field:Pattern for example.
The reason for this is twofold, on the one hand you place the annotations on Kotlin properties, and Kotlin properties kan be accessed in a variety of ways. Therefore, you need to specify how you want to access the property to apply the annotation on. On the other hand, the annotations have a set of predefined targets. You can inspect the annotation to see for example that it has a target of field. That's why we can use the #field:Pattern and #field:Email.
This is a key difference with java, where you have have distinct getters, setters, and fields amongst others.

Related

spring-data-neo4j v6: No converter found capable of converting from type [MyDTO] to type [org.neo4j.driver.Value]

Situation
I'm migrating a kotlin spring data neo4j application from spring-data-neo4j version 5.2.0.RELEASE to version 6.0.11.
The original application has several Repository interfaces with custom queries which take some DTO as a parameter, and use the various DTO fields to construct the query. All those types of queries currently fail with
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [MyDTO] to type [org.neo4j.driver.Value]
The reference documentation for spring-data-neo4j v6 only provides examples where parameters passed to custom query methods of a #Repository interface are of the same type as the #Node class associated with that repository. The documentation does not explicitly state that only parameters of the Node class are allowed.
Question
Is there any way to pass an arbitrary DTO (not being a #Node class) to a custom query method in a #Repository interface in spring-data-neo4j v6 like it was possible in v5?
Code samples
Example node entity
#Node
data class MyEntity(
#Id
val attr1: String,
val attr2: String,
val attr3: String
)
Example DTO
data class MyDTO(
val field1: String,
val field2: String
)
Example Repository interface
#Repository
interface MyRepository : PagingAndSortingRepository<MyEntity, String> {
// ConverterNotFoundException is thrown when this method is called
#Query("MATCH (e:MyEntity {attr1: {0}.field1}) " +
"CREATE (e)-[l:LINK]->(n:OtherEntity {attr2: {0}.field2))")
fun doSomethingWithDto(dto: MyDTO)
}
Solutions tried so far
Annotate DTO as if it were a Node entity
Based on the following found in the reference docs https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom-queries.parameters
Mapped entities (everything with a #Node) passed as parameter to a
function that is annotated with a custom query will be turned into a
nested map.
#Node
data class MyDTO(
#Id
val field1: String,
val field2: String
)
Replace {0} with $0 in custom query
Based on the following found in the reference docs https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom-queries.parameters
You do this exactly the same way as in a standard Cypher query issued
in the Neo4j Browser or the Cypher-Shell, with the $ syntax (from
Neo4j 4.0 on upwards, the old {foo} syntax for Cypher parameters has
been removed from the database).
...
[In the given listing] we are referring to the parameter by its name.
You can also use $0 etc. instead.
#Repository
interface MyRepository : PagingAndSortingRepository<MyEntity, String> {
// ConverterNotFoundException is thrown when this method is called
#Query("MATCH (e:MyEntity {attr1: $0.field1}) " +
"CREATE (e)-[l:LINK]->(n:OtherEntity {attr2: $0.field2))")
fun doSomethingWithDto(dto: MyDTO)
}
Details
spring-boot-starter: v2.4.10
spring-data-neo4j: v6.0.12
neo4j-java-driver: v4.1.4
Neo4j server version: v3.5.29
RTFM Custom conversions ...
Found the solution myself. Hopefully someone else may benefit from this as well.
Solution
Create a custom converter
import mypackage.model.*
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.neo4j.driver.Value
import org.neo4j.driver.Values
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair
import java.util.HashSet
class DtoToNeo4jValueConverter : GenericConverter {
override fun getConvertibleTypes(): Set<ConvertiblePair>? {
val convertiblePairs: MutableSet<ConvertiblePair> = HashSet()
convertiblePairs.add(ConvertiblePair(MyDTO::class.java, Value::class.java))
return convertiblePairs
}
override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor?): Any? {
return if (MyDTO::class.java.isAssignableFrom(sourceType.type)) {
// generic way of converting an object into a map
val dataclassAsMap = jacksonObjectMapper().convertValue(source as MyDTO, object :
TypeReference<Map<String, Any>>() {})
Values.value(dataclassAsMap)
} else null
}
}
Register custom converter in config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.neo4j.core.convert.Neo4jConversions
import org.springframework.core.convert.converter.GenericConverter
import java.util.*
#Configuration
class MyNeo4jConfig {
#Bean
override fun neo4jConversions(): Neo4jConversions? {
val additionalConverters: Set<GenericConverter?> = Collections.singleton(DtoToNeo4jValueConverter())
return Neo4jConversions(additionalConverters)
}
}
It's ridiculous that the framework would force you to write a custom converter for this. I made a #Transient object in my overridden User class for a limited set of update-able user profile fields, and I'm encountering the same error. I guess I will just have to break up the object into its component String fields in the method params to get around this problem. What a mess.
#Query("MATCH (u:User) WHERE u.username = :#{#username} SET u.firstName = :#{#up.firstName},u.lastName = :#{#up.firstName},u.intro = :#{#up.intro} RETURN u")
Mono<User> update(#Param("username") String username,#Param("up") UserProfile up);
No converter found capable of converting from type [...UserProfile] to type [org.neo4j.driver.Value]

Javax Validation Custom enum constrains in Kotlin

I'm trying to create a custom annotation and validator to use in conjunction with the javax validation Api and I'm having trouble access the values of an enum.
The objective of the annotation and the validator is validate if an input data is present within the enum values.
This is the annotation class
import javax.validation.Constraint
import javax.validation.Payload
import kotlin.reflect.KClass
#kotlin.annotation.Target(
AnnotationTarget.FIELD,
)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MustBeDocumented
#Constraint(validatedBy = [ValueOfEnumValidator::class])
annotation class ValueOfEnum(
val enumClass: KClass<Enum<*>>,
val message: String ="",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
This is the validator implementation
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
class ValueOfEnumValidator: ConstraintValidator<ValueOfEnum, CharSequence> {
private val acceptedValues: MutableList<String> = mutableListOf()
override fun initialize(constraintAnnotation: ValueOfEnum) {
super.initialize(constraintAnnotation)
acceptedValues.addAll(constraintAnnotation.enumClass.java
.enumConstants
.map {it.name}
)
}
override fun isValid(value: CharSequence?, context: ConstraintValidatorContext): Boolean {
return if (value == null) {
true
} else acceptedValues.contains(value.toString())
}
}
I'm aiming to use annotation like this:
#field:ValueOfEnum(enumClass = SortDirectionEnum::class, message = "{variants.sorted.sort.direction.not.valid}")
var sortDirection:String?=
But my IDE is reporting me the following error in the enumClass parameter
Type mismatch.
Required:KClass<Enum<*>>
Found:KClass<SortDirectionEnum>
How can I make the annotation generic enough to support different enums, and fix this issue ?
You are restricting enumClass to instances of Enum<*>, allowing Enum instances (Enum is an abstract class though, so nothing can be used) with all types of data, you however want to also allow child classes of Enum, which can be achieved with the out keyword there.
val enumClass: KClass<out Enum<*>>,
https://kotlinlang.org/docs/generics.html

Spring custom #ConditionalOnProperty annotation can not use #AliasFor

I want to create FeatureFlag annotation for my project to avoid code repetitions.
I created a new annotation called FeatureFlag. I decorated with ConditionalOnProperty annotation with the generic prefix foo.features. I add new fields to the annotation, which is AliasFor the ConditionalOnProperty fields. As far as I know, the following code should work, but it does not. I also tested the aliasing on the Profile annotation and that is working.
import io.kotlintest.shouldBe
import org.junit.jupiter.api.Test
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.AliasFor
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
#ConditionalOnProperty(prefix = "foo.features")
annotation class FeatureFlag(
#get:AliasFor(annotation = ConditionalOnProperty::class, value = "name") val feature: String,
#get:AliasFor(annotation = ConditionalOnProperty::class, value = "havingValue") val enabled: String = "true"
)
#SpringBootTest(
properties = ["foo.features.dummy: true"],
classes = [FeatureFlagTest.FeatureFlagTestConfiguration::class]
)
class FeatureFlagTest(private val applicationContext: ApplicationContext) {
#Configuration
class FeatureFlagTestConfiguration {
#Bean
#FeatureFlag(feature = "dummy")
fun positive(): String = "positive"
#Bean
#FeatureFlag(feature = "dummy", enabled = "false")
fun negative(): String = "negative"
}
#Test
fun `test`() {
applicationContext.getBean(String::class.java) shouldBe "positive"
}
}
When I running the test case I get the following error:
java.lang.IllegalStateException: The name or value attribute of #ConditionalOnProperty must be specified
(The FeatureFlag annotation should contain the value of the name field.)
Can you help, what did I wrong? Is it a bug in the framework?
In addition to the prefix attribute, you also need to define the name or value attribute in the ConditionalOnProperty annotation in order for it to work.
Have a look here to see the details of the annotation you're using: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.html

Spring Validation of JSON - Why do I need to add `#field`

I've finally made some progress on Spring validation (on a JSON object coming in from RabbitMQ).
However there are a couple of things I don't understand:
In the documentation, it states I can just use the annotation #NotBlank then in my method I use the annotation #Valid. However I find this wasn't doing anything. So instead I did #field:NotBlank and it worked together with the following - why did this #field do the trick?
#JsonIgnoreProperties(ignoreUnknown = true)
data class MyModel (
#field:NotBlank(message = "ID cannot be blank")
val id : String = "",
#field:NotBlank(message = "s3FilePath cannot be blank")
val s3FilePath : String = ""
)
Then the function using this model:
#Service
class Listener {
#RabbitListener(queues = ["\${newsong.queue}"])
fun received(data: MyModel) {
val factory = Validation.buildDefaultValidatorFactory()
val validator = factory.validator
val validate = validator.validate(data)
// Then this `validate` will return an array of validation errors
println(validate)
}
}
Correct me if I'm wrong however I assumed just using #Valid and this point fun received(#Valid data: MyModel) it would just throw some exception for me to catch - any idea based on my code why this could have been?
Any advice/help would be appreciated.
Thanks.
Here are the imports:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import javax.validation.*
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.amqp.rabbit.annotation.RabbitListener
import javax.validation.constraints.NotBlank
Quoting Kotlin's documentation for annotations:
When you're annotating a property or a primary constructor parameter, there are multiple Java elements which are generated from the corresponding Kotlin element, and therefore multiple possible locations for the annotation in the generated Java bytecode. To specify how exactly the annotation should be generated, use the following syntax:
class Example(#field:Ann val foo, // annotate Java field
#get:Ann val bar, // annotate Java getter
#param:Ann val quux) // annotate Java constructor parameter
So, until explicitly mention what you are annotating (field, getter or something else) in Kotlin class constructor, it won't automatically know where you want to put that annotation.

#RequestBody is getting null values

I have created a simple REST service (POST). But when i call this service from postman #RequestBody is not receiving any values.
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
#RestController
public class Add_Policy {
#ResponseBody
#RequestMapping(value = "/Add_Policy", headers = {
"content-type=application/json" }, consumes = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST)
public Policy GetIPCountry( #RequestBody Policy policy) {
System.out.println("Check value: " + policy.getPolicyNumber());
return policy;
}
}
My java Bean object is like below:
public class Policy {
private String PolicyNumber;
private String Type;
private String Tenture;
private String SDate;
private String HName;
private String Age;
public String getPolicyNumber() {
return PolicyNumber;
}
public void setPolicyNumber(String policyNumber) {
PolicyNumber = policyNumber;
}
public String getType() {
return Type;
}
public void setType(String type) {
Type = type;
}
public String getTenture() {
return Tenture;
}
System.out.println is printing a null as a value for PolicyNumber.
Please help me to resolve this issue.
JSON which i am passing in request body is
{
"PolicyNumber": "123",
"Type": "Test",
"Tenture": "10",
"SDate": "10-July-2016",
"HName": "Test User",
"Age": "10"
}
I have even set Content-Type to application/json in postman
Check the #RequestBody import,
wrong that will cause the problem in most cases.
import io.swagger.v3.oas.annotations.parameters.RequestBody;
to solve problem It should be
import org.springframework.web.bind.annotation.RequestBody;
Try setting the first character of the properties in your JSON to lower case. Eg.
{
"policyNumber": "123",
"type": "Test",
"tenture": "10",
"sDate": "10-July-2016",
"hName": "Test User",
"age": "10"
}
Basically, Spring uses getter and setter to set the properties of the the bean object. And it takes the property of the JSON object, matches it with the setter of the same name. eg to set the policyNumber property it tries to find a setter with the name setpolicyNumber() in your bean class and use that to set the value of your bean object.
Setter would have been missed. So, Object values do not get set.
If you are not in power to change the JSON format and still want to fix this problem, try adding
#JsonNaming(PropertyNamingStrategy.UpperCamelCaseStrategy.class)
annotation before your DTO (Policy in example) class.
Java convention demands the name of variable in a POJO (attribute of a class) must to be the first character in lowercase.
You have uppercase letters in your JSON properties, which is what is causing the failure.
I had lombok in my pom, and lombok annotations on my bean. I did not properly installed lombok with my STS yet, and had similar issue, my bean was not populated.
When I removed lombok annotations, my bean was properly populated.
Seems like a combination of lomboc not properly installed on STS + lomboc annotations on my bean.
if you are using Lombok Api then there are no Getters and Setters publicly visible or available to the #ResponseBody and #RequestBody annotation.
That is why we read the JSON request with null values.
So you need to comment those #Getter, #Setter annotation to Receive JSON response and Read JSON request object and generate the same getters and setters.
Restart or Hot Load (using Spring-Boot-Devtools) server and it should work for sure.
You can still use your lombok api for other Entities or BO's.
In my case was a Lombok issue. I removed all the lombok annotations and created the constructor, setter and getter manually.
As an advise, I would also set the JSON to lowercase to follow the convention.
Use the annotation org.springframework.web.bind.annotation.RequestBody and not org.springframework.web.bind.annotation.ResponseBody
In my case, empty constructor must be defined.
public MyClass(){}
Apart from lowerCamelCasing, for me what additionally needed was applying #JsonProperty(value="your expected JSON KEY name") for each of the getter and setter methods and using this operator under the POJO/Bean/DTO class.
Sample Code:
#JsonProperty(value="policyNumber")
public void setPolicyNumber(String policyNumber) {
this.policyNumber = policyNumber;
}
Had the same issue but for my case only one field was not being set. A log on the request body object showed it was being recieved as null. deleted getters and setters for the field and autogenerated them using the IDE and all worked fine.
I highly suspect a mismatch in the getter and setter definition can also cause this
I have been having this issue too, but the best way i solve mine was checking on spaces after the first quotes in every initialization of fields in my json values
see spring PropertyNamingStrategy(UPPER_CAMEL_CASE,LOWER_CAMEL_CASE ,LOWER_CASE
etc... defalult SNAKE_CASE).
Spring will auto change http contorller class parameter by naming strategy, which may be not consistant with your request json
take SNAKE_CASE as a ex, when "myToken" in java controller class, you client should send my_token instead of myToken
If you are using Lombok you need compileOnly and annotationProcessor
In my case I missed the 2nd one. So I got all null values
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
1-Make Entity class properties start with lowercase.
2-Check for Annotations.
3-Check for Constructor--> **Entity classes should have two constructor.
4-Check for Getter and Setters.
In my case, date format was given incorrectly

Resources