Comma-delimited request params not working with Kotlin data class - spring-boot

I have an endpoint which can accept dozens of request params. It would be nice to collect all these params in a data class. Additionally, the request params must be separated by commas
path?param=1,2
to keep backward compatibility.
Assume we have this endpoint:
#GetMapping("path")
fun someFun(#RequestParam param: Set<Int> = emptySet()
...other 11 params
)
I created a data class to collect all the request params:
data class ClusteredParams(val param: Set<Int> = emptySet()
...other 11 params
)
So the endpoint looks like the following:
#GetMapping("path")
fun someFun(param: ClusteredParams)
When I called path?param=1,2 I got:
"error": "Failed to convert value of type java.lang.String[] to required type java.util.Set; nested exception is java.lang.NumberFormatException: For input string: "1,2"",
"field": "param",
"rejectedValue": "1,2"
When I call path?param=1&param=2 everything is fine. This problem does not exist when ClusteredParams class is written in Java.
public class ClusteredParams {
private Set<Int> param;
...other 11 params
getters and setters
}

I don't know the answer, but I did the comparison what works and when:
in Java:
You can either call ?path=1,2 and ?path=1&path=2 no matter if your request param is declared using #RequestParam path: List<Int> inside signature of controller method, or it is a field of external object that's used to group all request params of controller method.
In Kotlin:
if you declare your request param inside of controller method signature, as a #RequestParam path: List<Int> or whatever else, you can call it using both methods, so:
?path=1,2 and ?path=1&path=2
If you however extract it to separate data class, and then expect this class to be populated in controller method, you can only do the ?path=1&path=2, because in the case of ?path=1,2 you get:
org.springframework.validation.BindException because it tries to parse entire '1,2' instead of split it first.
I hope now it is more clear, does anyone have idea why this happens?

Instead of val use var inside data class body:
data class ClusteredParams(
...other 11 params
) {
var param: Set<Int> = emptySet()
}

Related

Can #PathVariable recieve variable defined own orignal Data type?

I made controller method.
I want the method to receive variable defined by own original Data type.
like Below,
data class UserId(
val value: UUID
)
#GetMapping("user/{userId}")
fun getUser(
#PathVariable userId: UserId
) {
userService.getUser(userId)
}
Of course, I know how to receive variable of String.
#GetMapping("user/{userId}")
fun getUser(
#PathVariable userId: String
) {
// I think this code is redundancy.
val id = UserId.fromString(userId)
userService.getUser(userId)
}
Can I receive variable defined own original Data Type?
Do you know any idea?
The main question is, how do you see this working? Would you receive the data class as a serializable JSON object? If so, shouldn't that be inputted as the request body?
If there's another way you envision this working, you can always manually serialize the object later, something like:
Controller:
#GetMapping("user/{userId}")
fun getUser(
#PathVariable userIdSerialized: String
) {
userService.getUser(userIdSerialized)
}
Service:
fun getUser(userIdSerialized: String) {
// Using Jackson
val deserialized: UserId = mapper.readValueFromString(userIdSerialized, UserId::class.java)
}
But again, this should really be a request body.

Javax validation of generics in Springboot with Kotlin

I have a controller:
#PostMapping
fun create(
#RequestBody #Valid request: MyContainer<CreateRequest>,
): MyContainer<Dto> = service.create(request.objects)
with MyContainer and CreateRequest looking something like this:
class MyContainer<T>(
#field:Valid // also tried param
#field:NotEmpty(message = "The list of objects can not be null or empty")
var objects: List<#Valid T>? = listOf(),
)
class CreateRequest(
#field:NotNull(message = "Value can not be null")
var value: BigDecimal? = null,
)
In my tests, the "outer" validation works, that is I do get the expected error message if I send it { "objects": null } or { "objects": [] }. But I can not get it to validate the contents of the list. From what I understand in Java List<#Valid T> should work, but for whatever I can not get it to work in kotlin.
I figured I might need some kind of use-site target on #Valid in List<#Valid T>, but I can't find one that's applicable for this use case.
How can I get the validation to work for the list?
I managed to find a solution myself.
Apparently get: is the correct use-site target, not field: or param:. Furthermore the #Valid in List<#Valid T> was not necessary.
For reference, here's the working class (also changed it back to a data class as that doesn't seem to pose an issue).
class MyContainer<T>(
#get:Valid
#get:NotEmpty(message = "The list of objects can not be null or empty")
var objects: List<T>? = listOf(),
)
and the CreateRequest:
class CreateRequest(
#get:NotNull(message = "Value can not be null")
var value: BigDecimal? = null,
)
Changing to the get: use-site target was only necessary for #Valid, but I opted for using it everywhere for consistency and since it seems to be the one that works best.

Spring Boot Ajax GET Request with Array Parameter, Received as List: "No primary or default constructor found for interface java.util.List"

I'm sending to a Spring Controller an Ajax GET request with an Array Parameter,
$.ajax({
url: "getChoices",
dataType: "json",
type: "get",
data: {
'myarg': myarray // JS array of strings, ["a","b","c"]
// Verified to be correct
},
Controller Method that should receive this argument -- the arg name matches:
#ResponseBody
#GetMapping("/getChoices")
public List<KeyValueBean> getChoices(List<String> myarg) {
//...
}
First of all, the way this is written above, I'm getting the error:
[java.lang.IllegalStateException: No primary or default constructor found for interface
java.util.List]
Caused by: java.lang.NoSuchMethodException: java.util.List.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.createAttribute(ModelAttributeMethodProcessor.java:216)
Then I tried making some tweaks:
1) Made the method signature a String[], that's not what I want -- I need a List -- but tried it just in case:
public List<KeyValueBean> getChoices(String[] myarg) {
This came into the method, but myarg is NULL and didn't get set correctly.
2) Kept a List<String> but tried JSON.stringify around the array:
JSON.stringify(myarray)
Same exception: [java.lang.IllegalStateException: No primary or default constructor found for interface java.util.List]
How do I pass an Array arg in an Ajax GET request in Spring Boot?
You forgot to annotate your parameter with #RequestParam.
#GetMapping("/testList")
public void test(#RequestParam("myarg") List<String> myarg) {
for (String str : myarg) {
System.out.println(str);
}
}
You can fire a request to your endpoint as following and it will work.
http://localhost:8080/testList?myarg=abc,def
The only thing that worked for me is to specify [] in #RequestParam value:
#ResponseBody
#GetMapping("/getChoices")
public List<KeyValueBean> getChoices(#RequestParam(value="myarg[]")String[] myarg) {
//...
}
This is atually explained here, https://stackoverflow.com/a/5702905/1005607
it is due to a parameter naming incompatibility between Spring and
jQuery, where jQuery wants to put square brackets in to indicate that
a parameter is an array (I think PHP likes this too), but where Spring
doesn't care.

Returning Flux<String> from Spring WebFlux returns one string instead of array of strings in JSON

new to Spring WebFlux, trying to return array of strings in one endpoint and for some reason it returns one concatenated string istead of JSON array.
Wrapping it with some class solves the problem but wonder how to actually return array of strings? Returning for example Array<String> works as expected
class Wrapper(val data: String) {
#RestController
class Test() {
#RequestMapping("/wrapped") // Returns valid JSON array: [{"value":"Hello"},{"value":"World"}]
fun b() = Flux.just(Wrapper("Hello"),Wrapper("World"))
#RequestMapping("/raw") // Returns not valid JSON with just one concatenated string: HelloWorld
fun a() = Flux.just("Hello", "World")
}
Got an answer from Sébastien Deleuze (Spring framework committer) in Twitter https://twitter.com/sdeleuze/status/956136517348610048
Indeed when element type is String, the handler method is expected to provide directly well formed JSON String chunks, no serialization with Jackson is involved.

Kotlin not nullable value can be null?

I have backend that return me some json.
I parse it to my class:
class SomeData(
#SerializedName("user_name") val name: String,
#SerializedName("user_city") val city: String,
var notNullableValue: String
)
Use gson converter factory:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(ENDPOINT)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
And in my interface:
interface MyAPI {
#GET("get_data")
Observable<List<SomeData>> getSomeData();
}
Then I retrieve data from the server (with rxJava) without any error. But I expected an error because I thought I should do something like this (to prevent GSON converter error, because notNullableValue is not present in my JSON response):
class SomeData #JvmOverloads constructor(
#SerializedName("user_name") val name: String,
#SerializedName("user_city") val city: String,
var notNullableValue: String = ""
)
After the data is received from backend and parsed to my SomeData class with constructor without def value, the value of the notNullableValue == null.
As I understand not nullable value can be null in Kotlin?
Yes, that is because you're giving it a default value. Ofcourse it will never be null. That's the whole point of a default value.
Remove ="" from constructor and you will get an error.
Edit: Found the issue. GSON uses the magic sun.misc.Unsafe class which has an allocateInstance method which is obviously considered very unsafe because what it does is skip initialization (constructors/field initializers and the like) and security checks. So there is your answer why a Kotlin non-nullable field can be null. Offending code is in com/google/gson/internal/ConstructorConstructor.java:223
Some interesting details about the Unsafe class: http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
Try to override constructor like this:
class SomeData(
#SerializedName("user_name") val name: String,
#SerializedName("user_city") val city: String,
var notNullableValue: String = "") {
constructor() : this("","","")
}
Now after server response you can check the notNullableValue is not null - its empty

Resources