'Custom parameter names with a bean for request parameters in Spring 5
I'm trying to use Spring 5 to have a custom bean for my request parameters. In theory this is easy, but I want to have the field names be different from the parameter names.
I can do that trivially with normal @RequestParam
parameters, but I can't seem to get it to work with a bean.
I've found this question asked before, and the answer seems to be "Do it manually", with various different options for automating that, e.g. using Argument Resolvers. But is this really still the case in Spring 5?
My code (It's Kotlin btw, but that shouldn't matter) is like this:
data class AuthorizationCodeParams(
@RequestParam("client_id") val clientIdValue: String?,
@RequestParam("redirect_uri") val redirectUriValue: String?,
@RequestParam("scope") val scopes: String?,
@RequestParam("state") val state: String?
)
fun startAuthorizationCode(params: AuthorizationCodeParams): ModelAndView {
Solution 1:[1]
It seems there's no Spring-provided way of doing this.
There's an open issue on the Spring GitHub page: https://github.com/spring-projects/spring-framework/issues/25815
I found some solutions on StackOverflow, for example https://stackoverflow.com/a/16520399/4161471, but they seemed quite complex, or not re-usable. So I developed a workaround.
Workaround: Parse as JSON
I resolved this by creating a custom annotation and a HandlerMethodArgumentResolver
. The argument resolver will translate the request parameters using Jackson. (Using Jackson is not required, but it was nice and generic and safe.)
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.core.MethodParameter
import org.springframework.stereotype.Component
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer
/**
* Parses all request parameters and maps them to a JSON-serializable class.
*
* This is useful, because it allows multiple `@RequestParam`s to be
* encapsulated in a single object, the values will be type-safe, and
* the parameter names can be set using (for example, with `@JsonProperty`).
*/
@Target(AnnotationTarget.VALUE_PARAMETER)
@MustBeDocumented
annotation class JsonRequestParam {
@Component
class ArgumentResolver(
private val objectMapper: ObjectMapper
) : HandlerMethodArgumentResolver {
// only resolve parameters that are annotated with JsonRequestParam
override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.hasParameterAnnotation(JsonRequestParam::class.java)
}
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): Any? {
// `webRequest.parameterMap` is a `Map<String, String[]>`, so join
// the values to a string
val params: Map<String, String> = webRequest.parameterMap.mapValues { (_, v) -> v.joinToString() }
return objectMapper.convertValue(params, parameter.parameterType)
}
}
}
Note that joining the parameter map values together with webRequest.parameterMap.mapValues { (_, v) -> v.joinToString() }
is a quick fix. It's probably not correct and will only work for classes with primitive parameters - please suggest improvements!
Example usage
So if I have a DTO...
data class MyDataObject(
@JsonProperty("n")
val name: String,
val age: Int,
)
I've used @JsonProperty
to override the property name, which means the request parameter for 'name' will be n
.
Now in my @Controller
I can use the DTO in a @GetMapping
method, and annotate it with the @JsonRequestParam
I created.
import my.project.JsonRequestParam
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api")
class MyDataController {
@GetMapping(value = ["/data"])
fun getData(
@JsonRequestParam
request: MyDataObject
): ResponseEntity<String> {
return "get data $request"
}
}
So if I GET /api/data?n=MyName&age=22
, then I'll get a response:
get data MyDataObject(name = "MyName", age = 22)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |