I am trying to add validation to my data models in kotlin, The simple fields are easy to do using the #field annotations. But, I am struggling to do the same with collections.
I have uploaded the issue to github here
The java model is working with no issues but the kotlin version is not. I am adding both the models here.
public class JavaUser {
#NotEmpty
#NotNull
#Pattern(regexp = "[a-z]*", message = "Only lower case first name")
private String name;
private List<
#NotNull
#NotEmpty
#Pattern(regexp = "\\d{10}", message = "Only 10 digits")
String> phones;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getPhones() {
return phones;
}
public void setPhones(List<String> phones) {
this.phones = phones;
}
}
data class KotlinUser(
#field:NotEmpty
#field:NotNull
#field:Pattern(regexp = "[a-z]*", message = "Only lower case first name")
val name: String,
// Cannot use #field here, anything else we could use?
val phones: List<
#NotNull
#NotEmpty
#Pattern(regexp = "\\d{10}", message = "Only 10 digits")
String>
)
My tests - The java test passes but the kotlin one fails
#Test
fun `java user validation`() {
val javaUser = JavaUser()
javaUser.name = "sadfjsjdfhsjdf"
javaUser.phones = listOf("dfhgd")
webTestClient.put().uri("/user/java")
.body(BodyInserters.fromObject(javaUser))
.exchange()
.expectStatus().is4xxClientError
}
#Test
fun `kotlin user validation`() {
val kotlinUser = KotlinUser(name = "sadfjsjdfhsjdf", phones = listOf("dfhgd"))
webTestClient.put().uri("/user/kotlin")
.body(BodyInserters.fromObject(kotlinUser))
.exchange()
.expectStatus().is4xxClientError
}
Controller
#RestController
class Controller {
#PutMapping("/user/java")
fun putUser(#RequestBody #Valid javaUser: JavaUser): Mono<ResponseEntity<String>> =
Mono.just(ResponseEntity("shouldn't get this", HttpStatus.OK))
#PutMapping("/user/kotlin")
fun putUser(#RequestBody #Valid kotlinUser: KotlinUser): Mono<ResponseEntity<String>> =
Mono.just(ResponseEntity("shouldn't get this", HttpStatus.OK))
}
Any help would be greatly appreciated. Thank you!
This is currently not supported. The Kotlin compiler currently ignores annotations on types.
See for details:
https://discuss.kotlinlang.org/t/i-have-a-question-about-applying-bean-validation-2-0/5394
Kotlin data class and bean validation with container element constraints
There are also issues on the Kotlin issue tracker for this:
https://youtrack.jetbrains.net/issue/KT-26605
https://youtrack.jetbrains.net/issue/KT-13228
The latter has target version 1.4.
Try this:
data class KotlinUser(
#field:NotEmpty
#field:NotNull
#field:Pattern(regexp = "[a-z]*", message = "Only lower case first name")
val name: String,
#field:Valid
#field:NotEmpty
val phones: List<Phone>
)
data class Phone(
#NotBlank
#Pattern(regexp = "\\d{10}", message = "Only 10 digits")
val phoneNumber: String?
)
If you don't stick to Bean Validation, YAVI can be an alternative.
You can define the validation as follows:
import am.ik.yavi.builder.ValidatorBuilder
import am.ik.yavi.builder.forEach
import am.ik.yavi.builder.konstraint
data class KotlinUser(val name: String,
val phones: List<String>) {
companion object {
val validator = ValidatorBuilder.of<KotlinUser>()
.konstraint(KotlinUser::name) {
notEmpty()
.notNull()
.pattern("[a-z]*").message("Only lower case first name")
}
.forEach(KotlinUser::phones, {
constraint(String::toString, "value") {
it.notNull().notEmpty().pattern("\\d{10}").message("Only 10 digits")
}
})
.build()
}
}
Here is a YAVI version of your code.
A workaround there is to use the #Validated from spring in your controller.
Example:
#RestController
#RequestMapping("/path")
#Validated
class ExampleController {
#PostMapping
fun registerReturns(#RequestBody #Valid request: List<#Valid RequestClass>) {
println(request)
}
}
RequestClass:
#JsonIgnoreProperties(ignoreUnknown = true)
data class RequestClass(
#field:NotBlank
val field1: String?,
#field:NotBlank
#field:NotNull
val field2: String?)
This way Beans Validation is applied.
Related
Essentially, I'm trying to get something like this
public class Post {
public final #Nullable Optional<#Size(min = 10) String> title;
public Post(#JsonProperty("title") Optional<String> title) {
this.title = title;
}
}
but in Kotlin.
I've tried
class Post(
#field:Size(min = 10) val title: Optional<String>?
)
but that annotates the title property and not the String inside the Optional.
Is there a way to achieve this in Kotlin?
The context of this is to use validators in Spring Boot to validate the given string, if it is present. The Optional is there to allow me to distinguish between the user sending
{
"title": null
}
or
{}
I'm new in programming and I'm had having some problems to create an API for Kotlin getting data from a existing DB from mongoDB to my API.
I'm making Tests and I want to return a data with it Id, but I can't.
I'm using the SpringBoot, JPA, and mongoDB Data. The host is my machine, the database name is ArthurLeywin, and the collection name is Capitulos.
I'm receiving the error status=404, error=Not Found, path=/ArthueLeywin/Capitulos/6306e6417d5b2e259951f292}
The code of the Test is
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ExtendWith(SpringExtension::class)
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TbateMobileReederApplicationTests #Autowired constructor(
private val capituloRepository: CapituloRepository,
private val restTemplate: TestRestTemplate
) {
private val defaultCapituloId : ObjectId = ObjectId("6306e6417d5b2e259951f292")
//ObjectId.get()
#LocalServerPort
protected var port: Int = 27017
private fun getRootUrl(): String? = "http://localhost:$port/ArthueLeywin/Capitulos"
#Test
fun `should return single cap by id`() {
val response = restTemplate.getForObject(
getRootUrl() + "/$defaultCapituloId",
Object::class.java
)
val titleResponse = response.toString()
println(titleResponse)
}
}
My controller, if it's needed is
#RestController
#RequestMapping("/Capitulos")
class CapituloController (
private val capituloRepository : CapituloRepository
){
#GetMapping
fun getAllCapitulos() : ResponseEntity<List<Capitulo>>{
val capitulos = capituloRepository.findAll()
return ResponseEntity.ok(capitulos)
}
#GetMapping("/{id}")
fun getOneCapitulo(#PathVariable("id") id: String): ResponseEntity<Capitulo> {
val capitulo = capituloRepository.findOneById(ObjectId(id))
return ResponseEntity.ok(capitulo)
}
#PostMapping
fun createCapitulo (#RequestBody request: CapituloRequest) : ResponseEntity<Capitulo>{
val capitulo = capituloRepository.save(Capitulo(
nomeCapitulo = request.nomeCapitulo,
urlString = request.urlString,
novelTexto = request.novelTexto
))
return ResponseEntity(capitulo, HttpStatus.CREATED)
}
}
As I said I'm new in this, so please help me with explanations <3
I'm trying to validate text parameter to be not blank in spring boot kotlin project, but its not working correctly and returning just response code 200 instead of 422.
org.springframework.boot:spring-boot-starter-validation" is also present in classpath.
Controller.kt
#Validated
#RestController
#RequestMapping(ApiEndPoints.Search.SEARCH_ROOT_PATH)
class SearchController(
private val searchService: SearchService
) {
#GetMapping(ApiEndPoints.Search.BY_USERS)
fun searchUsers(
#Valid #NotBlank(message = "Text must not be blank") #RequestParam text: String,
#RequestParam(defaultValue = Constants.DEFAULT_PAGE_0) page: Int,
#RequestParam(defaultValue = Constants.DEFAULT_SIZE_15) size: Int
): ResponseEntity<ApiResponse.Success<Map<String, Any>>> {
val users = searchService.searchUsers(text, page, size)
return ResponseEntity.ok(ApiResponse.Success(true, users.createResponseMapFromSlice(Constants.USERS_LABEL)))
}
}
ControllerAdvice.kt
#Order(Ordered.HIGHEST_PRECEDENCE)
#RestControllerAdvice
class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
val errors = hashMapOf<String, String>()
ex.bindingResult.allErrors.forEach {
val fieldName = (it as FieldError).field
val errorMessage = it.defaultMessage
errors[fieldName] = errorMessage.toString()
}
return ResponseEntity(
ApiResponse.ValidationError(
errors,
ErrorCodes.VALIDATION_FAILED
), HttpStatus.UNPROCESSABLE_ENTITY
)
}
}
I'm attempting to validate a String #PathVariable inside a Kotlin Spring controller.
#RestController
#Validated
#RequestMapping("/kotlin/")
open class KStringController {
#GetMapping(path = ["string/{username}"],
produces = [APPLICATION_JSON_VALUE])
#ResponseBody
fun validateStringPathVariable(
#Pattern(regexp = "[A-Za-z]+", message = "Username Pattern Validation Message")
#Size(min = 2, max = 15, message = "Username Size Validation Message")
#PathVariable("username") username: String?
): ResponseEntity<String>? {
logger.info { String.format("validateStringPathVariable: Got Username [%s]", username) }
System.out.printf("validateStringPathVariable: Username is [%s]%n", username)
return ResponseEntity.ok("Username is valid")
}
}
I have unit tests for it:
#ExtendWith(SpringExtension::class)
#WebMvcTest(KStringController::class)
#AutoConfigureMockMvc
class KStringGetControllerTest {
#field:Autowired
private lateinit var mvc: MockMvc
#Test
#Throws(Exception::class)
fun validStringPathVariable() {
val request: MockHttpServletRequestBuilder = givenARequestFor("/kotlin/string/mike")
val actions: ResultActions = whenTheRequestIsMade(request)
thenExpect(actions,
MockMvcResultMatchers.status().isOk,
MockMvcResultMatchers.content().bytes("Username is valid".toByteArray()))
}
#Test
#Throws(Exception::class)
fun shoutsWhenStringPathVariableIsTooShort() {
val request = givenARequestFor("/kotlin/string/a")
val actions = whenTheRequestIsMade(request)
val response = """{
"validationErrors": [
{
"fieldName": "validateStringPathVariable.username",
"message": "Username Size Validation Message"
}
]
}"""
val content = MockMvcResultMatchers.content()
thenExpect(actions,
MockMvcResultMatchers.status().isBadRequest,
content.contentType(MediaType.APPLICATION_JSON),
content.json(response))
}
private fun givenARequestFor(url: String): MockHttpServletRequestBuilder {
return MockMvcRequestBuilders.get(url)
.characterEncoding("UTF-8")
}
#Throws(Exception::class)
private fun whenTheRequestIsMade(request: MockHttpServletRequestBuilder): ResultActions {
return mvc.perform(request)
}
#Throws(Exception::class)
private fun thenExpect(resultActions: ResultActions, vararg matchers: ResultMatcher) {
resultActions.andExpect(ResultMatcher.matchAll(*matchers))
}
Currently the second test is failing; it's returning a 200 response code when I'm expecting a 400. Much much more code at https://github.com/Mavelous/spring-validation.
With help from others, I've managed to find a solution.
Quick answer: Change the function to open fun validateStringPathVariable, and the validations will start working.
Alternative answer: Use the org.jetbrains.kotlin.plugin.spring plugin in your build.gradle file.
I also created a blog post with a longer description: http://mavelo.us/2021/04/07/spring-validation-in-kotlin.html
I have the next structure of spring beans
abstract class GenericRepository<T> {
private val FIND_BY_ID_SQL = "SELECT * FROM ${this.getTableName()} WHERE id = ?"
abstract fun getTableName(): String
abstract fun jdbcTemplate(): JdbcTemplate
abstract fun getMapper(): RowMapper<T>
fun find(id: Long): T? {
return jdbcTemplate().queryForObject(FIND_BY_ID_SQL, arrayOf(id), getMapper())
}
}
User repository
#Repository
class UserRepository(
#Autowired
private val jdbcTemplate: JdbcTemplate
) : GenericRepository<User>() {
companion object {
private const val INSERT_SQL = "INSERT INTO \"user\"(name, age) VALUES (?,?)"
}
private class LogMapper : RowMapper<User> {
override fun mapRow(rs: ResultSet, rowNum: Int): User? {
return User(
id = rs.getLong("id"),
name = rs.getString("name"),
age = rs.getInt("operation")
)
}
}
override fun getTableName(): String {
return "user"
}
override fun jdbcTemplate(): JdbcTemplate {
return jdbcTemplate
}
override fun getMapper(): RowMapper<User> {
return LogMapper()
}
}
The problem when Spring creates proxy and creates bean of UserRepository it doesn't initialize FIND_BY_ID_SQL leaving it null.
The question: how usign abstract class make spring initialize FIND_BY_ID_SQL variable?
UPD
I used #Component instead of #Repository and the problem was solved. FIND_BY_ID_SQL is not null anymore.
You could work around the problem by making it lazy:
private val FIND_BY_ID_SQL by lazy { "SELECT * FROM ${this.getTableName()} WHERE id = ?" }
However, you should first be sure it's an actual problem (e.g. that when you call find you get an exception), because the proxy might simply delegate to a "real" UserRepository with non-null FIND_BY_ID_SQL (and jdbcTemplate etc.), depending on Spring's internal details.
In addition, you need to be careful when your superclass properties are initialized depending on subclass; I think your exact situation should work, but I'd prefer to write it as
abstract class GenericRepository<T>(val tableName: String) {
private val FIND_BY_ID_SQL = "SELECT * FROM ${tableName} WHERE id = ?"
abstract val jdbcTemplate: JdbcTemplate
abstract val mapper: RowMapper<T>
fun find(id: Long): T? {
return jdbcTemplate.queryForObject(FIND_BY_ID_SQL, arrayOf(id), mapper)
}
}
#Repository
class UserRepository(
#Autowired
override val jdbcTemplate: JdbcTemplate
) : GenericRepository<User>("user") { ... }