I have a problem with a test (mock) of type POST in kotlin, when i use a data class with a date field (LocalDate).
This is the Stack im using:
springBoot : v2.1.7.RELEASE
Java : jdk-11.0.4
kotlinVersion : '1.3.70'
junitVersion : '5.6.0'
junit4Version : '4.13'
mockitoVersion : '3.2.4'
springmockk : '1.1.3'
When i execute the POST method in the app, all is ok, i have the response and the data is saved correctly in the db:
curl -X POST "http://127.0.1.1:8080/v1/person/create" -H "accept: */*" -H "Content-Type: application/json" -d "[ { \"available\": true, \"endDate\": \"2090-01-02\", \"hireDate\": \"2020-01-01\", \"id\": 0, \"lastName\": \"stringTest\", \"name\": \"stringTest\", \"nickName\": \"stringTest\" }]"
But when i try to make the test of the POST Method, i cant (only with POST method, with GET is ok)
This are the classes that i use:
File Person.kt
#Entity
data class Person(
#Id #Column(name = "id") #GeneratedValue(strategy = GenerationType.AUTO)
var id: Long,
var name: String,
var lastName: String,
var nickName: String,
#JsonFormat(pattern = "yyyy-MM-dd")
var hireDate: LocalDate,
#JsonFormat(pattern = "yyyy-MM-dd")
var endDate: LocalDate,
var available: Boolean
) {
constructor() : this(0L, "Name example",
"LastName example",
"Nick example",
LocalDate.of(2020,1,1),
LocalDate.of(2090,1,1),
true)
File PersonService.kt
#Service
class PersonService(private val personRepository: PersonRepository) {
fun findAll(): List<Person> {
return personRepository.findAll()
}
fun saveAll(personList: List<Person>): MutableList<person>? {
return personRepository.saveAll(personList)
}
}
File PersonApi.kt
#RestController
#RequestMapping("/v1/person/")
class PersonApi(private val personRepository: PersonRepository) {
#Autowired
private var personService = PersonService(personRepository)
#PostMapping("create")
fun createPerson(#Valid
#RequestBody person: List<Person>): ResponseEntity<MutableList<Person>?> {
print("person: $person") //this is only for debug purpose only
return ResponseEntity(personService.saveAll(person), HttpStatus.CREATED)
}
}
And finally
PersonApiShould.kt (This class is the problem)
#EnableAutoConfiguration
#AutoConfigureMockMvc
#ExtendWith(MockKExtension::class)
internal class PersonApiShould {
private lateinit var gsonBuilder: GsonBuilder
private lateinit var gson: Gson
lateinit var mockMvc: MockMvc
#MockkBean
lateinit var personService: PersonService
#BeforeEach
fun setUp() {
val repository = mockk<PersonRepository>()
personService = PersonService(repository)
mockMvc = standaloneSetup(PersonApi(repository)).build()
gson = GsonBuilder()
.registerTypeAdapter(Person::class.java, PersonDeserializer())
.create()
gsonBuilder = GsonBuilder()
}
#AfterEach
fun clear() {
clearAllMocks()}
#Test
fun `create person`() {
val newPerson = Person(1L,
"string", //name
"string", //lastName
"string", //nickname
LocalDate.of(2020, 1, 1), //hireDate
LocalDate.of(2090, 1, 2), //endDate
true) //available
val contentList = mutableListOf<Person>()
contentList.add(newPerson)
// also tried with
// every { personService.findAll() }.returns(listOf<Person>())
// every { personService.saveAll(mutableListOf<Person>())}.returns(Person())
every { personService.findAll() }.returns(contentList)
every { personService.saveAll(any()) }.returns(contentList)
/* didn't work either
val personJson = gsonBuilder.registerTypeAdapter(Date::class.java, DateDeserializer())
.create().toJson(newPerson)
*/
val content = "[\n" +
" {\n" +
" \"available\": true,\n" +
" \"endDate\": \"2090-01-02\",\n" +
" \"hireDate\": \"2020-01-01\",\n" +
" \"id\": 0,\n" +
" \"lastName\": \"string\",\n" +
" \"name\": \"string\",\n" +
" \"nickName\": \"string\"\n" +
" }\n" +
"]"
val httpResponse = mockMvc.perform(post("/v1/resto/person/create")
.content(content) //also tried with .content(contentList)
.contentType(MediaType.APPLICATION_JSON))
.andReturn()
// error, because, httpResponse is always empty
val personCreated: List<Person> = gson.fromJson(httpResponse.response.contentAsString,
object : TypeToken<List<Person>>() {}.type)
assertEquals(newPerson.name, personCreated.get(0).name)
}
Gson have some issues when deserialize dates, this is a parser (hack), it works for my GET method
File PersonDeserializer.kt
class PersonDeserializer : JsonDeserializer<Person> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Person {
json as JsonObject
val name = json.get("name").asString
val lastName = json.get("lastName").asString
val nickName = json.get("nickName").asString
val available = json.get("available").asBoolean
val hireDate = LocalDate.of((json.get("hireDate") as JsonArray).get(0).asInt,
(json.get("hireDate") as JsonArray).get(1).asInt,
(json.get("hireDate") as JsonArray).get(2).asInt)
val endDate = LocalDate.of((json.get("endDate") as JsonArray).get(0).asInt,
(json.get("endDate") as JsonArray).get(1).asInt,
(json.get("endDate") as JsonArray).get(2).asInt)
return Person(1L, name, lastName, nickName, hireDate, endDate, available)
}
}
I see that the error is in the MOCKK Library, because from test i can reach the endpoint and print correctly the value
print from endpoint: print("person: $person") //this line is in the endpoint
Person: [Person(id=0, name=string, lastName=string, nickName=string, hireDate=2020-01-01, endDate=2090-01-02, available=true)]
Error test log
19:27:24.840 [main] DEBUG
io.mockk.impl.recording.states.AnsweringState - Throwing
io.mockk.MockKException: no answer found for:
PersonRepository(#1).saveAll([Person(id=0, name=string,
lastName=string, nickName=string, hireDate=2020-01-01,
endDate=2090-01-02, available=true)]) on
PersonRepository(#1).saveAll([Person(id=0, name=string,
lastName=string, nickName=string, hireDate=2020-01-01,
endDate=2090-01-02, available=true)])
19:27:24.844 [main] DEBUG
org.springframework.test.web.servlet.TestDispatcherServlet - Failed to
complete request: io.mockk.MockKException: no answer found for:
PersonRepository(#1).saveAll([Person(id=0, name=string,
lastName=string, nickName=string, hireDate=2020-01-01,
endDate=2090-01-02, available=true)])
org.springframework.web.util.NestedServletException: Request
processing failed; nested exception is io.mockk.MockKException: no
answer found for: PersonRepository(#1).saveAll([Person(id=0,
name=string, lastName=string, nickName=string, hireDate=2020-01-01,
endDate=2090-01-02, available=true)])
Errors varies, depending the fix, also i got
JSON parse error: Cannot deserialize value of type java.time.LocalDate from ...
... 48 more
But always is the same problem with serialization of LocalDate in Spring with Kotlin
Any assistance you can provide would be greatly appreciated.
After read a lot of posible solutions to this problem, i found some workarounds to handle this "issue".
Like i wrote, im using Gson, so, i´ve implemented an overrride for the serialization and another for the deserialization of LocalDates, also i found a hack(?) that override ToString() method in Data class, and more important, i found more issues when i tried to deserialize a post response with nulls in a LocalDate field, also i would like to say (again), that the problem were in the TEST NOT IN PRODUCTIVE CODE, let´s see:
1) Simple Get method, with not nulls
#Test
fun `return all non active persons`() {
val personList = givenAListOfpersons()
val activepersonsCount: Int = personList.filter { person ->
person.available==false }.size //2
every { personservice.findActivePersons() } returns personList
val httpResponse = mockMvc.perform(get("/v1/resto/person/list?available={available}", "false")
.param("available", "false")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(jsonPath("$", hasSize<Any>(activepersonsCount)))
.andReturn()
// Note: Simple deserialization: explain later
val response: List<person> = gsonDeserializer.fromJson(httpResponse.response.contentAsString,
object : TypeToken<List<person>>() {}.type)
assertEquals(personList.get(0).name, response.get(0).name)
assertEquals(personList.get(0).lastName, response.get(0).lastName)
assertEquals(personList.get(0).nickName, response.get(0).nickName)
assertEquals(personList.get(0).hireDate, response.get(0).hireDate)
assertEquals(personList.get(0).available, response.get(0).available)
}
2) Post method overriding ToString in data class with null values in endDate
a) Modify Data class
#Entity
data class person(
#Id #Column(name = "id") #GeneratedValue(strategy = GenerationType.AUTO)
var id: Long,
var name: String,
var lastName: String,
var nickName: String,
#JsonFormat(pattern = "yyyy-MM-dd")
var hireDate: LocalDate,
#JsonFormat(pattern = "yyyy-MM-dd")
var endDate: LocalDate?, //note this
var available: Boolean
) {
constructor() : this(0L, "xx",
"xx",
"xx",
LocalDate.of(2020,1,1),
null,
true)
//here
override fun toString(): String {
return "["+"{"+
'\"' +"id"+'\"'+":" + id +
","+ '\"' +"name"+'\"'+":"+ '\"' + name + '\"' +
","+ '\"' +"lastName"+'\"'+":"+ '\"' + lastName + '\"' +
","+ '\"' +"nickName"+'\"'+":"+ '\"' + nickName + '\"' +
","+ '\"' +"hireDate"+'\"'+":"+ '\"' + hireDate + '\"' +
","+ '\"' +"endDate"+'\"'+":"+ '\"' + endDate + '\"' +
","+ '\"' +"available"+'\"'+":" + available +
"}"+"]";
}
}
b) Test implementing toString() from Data class
#Test
fun `create person`() {
val personList = givenAListOfpersons() as MutableList<person>
every { personService.saveAll(any()) }.returns(personList)
val httpPostResponse = mockMvc.perform(post("/v1/resto/person/create")
.contentType(MediaType.APPLICATION_JSON)
.content(personTest.toString())) //THIS
.andDo(print())
.andExpect(status().isCreated) //It´s works!!
.andReturn()
// Note the gsonDeserializer, explain later
val personDeserializerToList = gsonDeserializer.fromJson<List<person>>(httpPostResponse.response.contentAsString,
object : TypeToken<List<person>>() {}.rawType).get(0) as LinkedTreeMap<String, Object>
assertEquals(personList.get(0).name, personDeserializerToList["name"])
assertEquals(personList.get(0).lastName, personDeserializerToList["lastName"])
assertEquals(personList.get(0).nickName, personDeserializerToList["nickName"])
assertEquals(personList.get(0).hireDate, personDeserializerToList["hireDate"]))
assertNull(personDeserializerToList["endDate"]))
assertEquals(personList.get(0).available, personDeserializerToList["available"])
}
3) Recommended way: Using Gson overrride Serialize method and format LocalDates:
#Test
fun `create person`() {
val personList = givenAListOfPersons() as MutableList<Person
// It´s work´s
val personSerializerToString = gsonSerializer.toJson(personList, object : TypeToken<List<person>>() {}.type)
every { personService.saveAll(any()) }.returns(personList)
val httpPostResponse = mockMvc.perform(post("/v1/resto/person/create")
.contentType(MediaType.APPLICATION_JSON)
.content(personSerializerToString))
.andDo(print())
.andExpect(status().isCreated) //It´s Work´s!
.andReturn()
// Deserialization problem: endDate is null, and we cant parse a null in Gson
// that´s why i use **rawType**
val personDeserializerToList = gsonDeserializer.fromJson<List<person>>(httpPostResponse.response.contentAsString,
object : TypeToken<List<person>>() {}.rawType).get(0) as LinkedTreeMap<String, Object>
assertEquals(personList.get(0).name, personDeserializerToList["name"])
assertEquals(personList.get(0).lastName, personDeserializerToList["lastName"])
assertEquals(personList.get(0).nickName, personDeserializerToList["nickName"])
// Note formatToLocalDate method: The date i receive from post is
// in this format ==> **[2020.0,1.0,1.0]** so i must to parse this
// format to LocalDate
assertEquals(personList.get(0).hireDate, formatToLocalDate(personDeserializerToList["hireDate"]))
assertNull(personDeserializerToList["endDate"])
assertEquals(personList.get(0).available, personDeserializerToList["available"])
}
Finally, Serialization, Deserialization and formatToLocalDate:
a) First, we have to set the configurations:
#ExtendWith(MockKExtension::class)
#EnableAutoConfiguration
#AutoConfigureMockMvc
internal class PersonApiShould {
private lateinit var gsonSerializer: Gson
private lateinit var gsonDeserializer: Gson
lateinit var mockMvc: MockMvc
#MockkBean
lateinit var personService: PersonService
#BeforeEach
fun setUp() {
val repository = mockk<PersonRepository>()
personService = PersonService(repository)
mockMvc = standaloneSetup(PersonApi(repository)).build()
// Note this
gsonDeserializer = GsonBuilder()
.registerTypeAdapter(Person::class.java, PersonDeserializer())
.create()
gsonSerializer = GsonBuilder()
.registerTypeAdapter(Person::class.java, PersonSerializer())
.create()
}
#AfterEach
fun clear() {
clearAllMocks()
}
tests ...
b) And the Methods
// This is because i receive [2020.0,1.0,1.0]
private fun formatToLocalDate(dates: Object?): LocalDate? {
return LocalDate.of(
((dates as ArrayList<Object>).get(0) as Double).toInt(),
((dates as ArrayList<Object>).get(1) as Double).toInt(),
((dates as ArrayList<Object>).get(2) as Double).toInt())
}
//Gson have some issues when deserialize dates, this is a parser (hack)
// This parser have some troubles handling null values, that´s why i use rawType instead,
//otherwise use this method
//Context: If we try to cast nulls in this class, we are going to receive this kind
// of errors
// ERROR with nulls:
//java.lang.ClassCastException: class com.google.gson.JsonNull cannot be cast to
//class
//com.google.gson.JsonArray (com.google.gson.JsonNull and
//com.google.gson.JsonArray are in unnamed module of loader 'app')
class PersonDeserializer : JsonDeserializer<Person?> {
override fun deserialize(jsonPersonResponse: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Person? {
jsonPersonResponse as JsonObject
val name = jsonPersonResponse.get("name").asString
val lastName = jsonPersonResponse.get("lastName").asString
val nickName = jsonPersonResponse.get("nickName").asString
val available = jsonPersonResponse.get("available").asBoolean
val hireDate = LocalDate.of((jsonPersonResponse.get("hireDate") as JsonArray).get(0).asInt,
(jsonPersonResponse.get("hireDate") as JsonArray).get(1).asInt,
(jsonPersonResponse.get("hireDate") as JsonArray).get(2).asInt)
// remember, this Gson, cant handle null values and endDate is usually null
val endDate = LocalDate.of((jsonPersonResponse.get("endDate") as JsonArray).get(0).asInt,
(jsonPersonResponse.get("endDate") as JsonArray).get(1).asInt,
(jsonPersonResponse.get("endDate") as JsonArray).get(2).asInt)
return Person(1L, name, lastName, nickName, hireDate, endDate, available)
}
}
//Gson have some issues when serializing dates, this is a parser (hack)
class PersonSerializer : JsonSerializer<Person> {
override fun serialize(src: Person, typeOfSrc: Type?, context: JsonSerializationContext): JsonObject {
val PersonJson = JsonObject()
PersonJson.addProperty("id", src.id.toInt())
PersonJson.addProperty("name", src.name)
PersonJson.addProperty("lastName", src.lastName)
PersonJson.addProperty("nickName", src.nickName)
PersonJson.addProperty("hireDate", src.hireDate.toString())
if (src.endDate != null) {
PersonJson.addProperty("endDate", src.endDate.toString())
} else {
PersonJson.addProperty("endDate", "".toShortOrNull())
}
PersonJson.addProperty("available", src.available)
return PersonJson
}
I hope this workaround could be useful.
Related
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 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.
I want inject a singleton in another class by kotlin in spring boot.
S.kt
#Singleton
#Component
class S(
private val userService: UserService,
val companyRepo: CompanyRepo
)
WorkingGroup.kt
class WorkingGroup(
override val name: String = "",
override val desc: String = ""
) : Csv() {
fun isCompatible(ct2: WorkingGroup): Boolean = this == ct2
companion object : ICsvEnumCompanion<WorkingGroup> {
#Inject
private lateinit var s: S
override val VALUES: List<WorkingGroup>
by lazy {
val details = s.user().company.details ?: CompanyDetails()
details.workingGroups.map { WorkingGroup(it.name, it.desc) }
}
}
}
By this code, I get below error:
Caused by: org.zalando.problem.DefaultProblem: Internal Server Error: lateinit property s has not been initialized
I search for this error and found some result like this, but the problem not solved.
How can I inject service in companion object in kotlin?
In order for Spring to inject into a companion object you will need to create a setter for the field outside of the companion object. WorkingGroup will need to be a Spring managed bean in order for Spring to autowire it (inject dependencies).
#Component
class WorkingGroup(
override val name: String = "",
override val desc: String = ""
) : Csv() {
fun isCompatible(ct2: WorkingGroup): Boolean = this == ct2
companion object : ICsvEnumCompanion<WorkingGroup> {
private lateinit var s: S
override val VALUES: List<WorkingGroup>
by lazy {
val details = s.user().company.details ?: CompanyDetails()
details.workingGroups.map { WorkingGroup(it.name, it.desc) }
}
}
#Autowired
fun setS(value: S) {
s = value;
}
}