6.5 Custom Argument Binding
Micronaut uses an ArgumentBinderRegistry to look up ArgumentBinder beans capable of binding to the arguments in controller methods. The default implementation looks for an annotation on the argument that is meta-annotated with @Bindable. If one exists the argument binder registry searches for an argument binder that supports that annotation.
If no fitting annotation is found Micronaut tries to find an argument binder that supports the argument type.
An argument binder returns a ArgumentBinder.BindingResult. The binding result gives Micronaut more information than just the value. Binding results are either satisfied or unsatisfied, and either empty or not empty. If an argument binder returns an unsatisfied result, the binder may be called again at different times in request processing. Argument binders are initially called before the body is read and before any filters are executed. If a binder relies on any of that data and it is not present, return a ArgumentBinder.BindingResult#UNSATISFIED result. Returning an ArgumentBinder.BindingResult#EMPTY or satisfied result will be the final result and the binder will not be called again for that request.
At the end of processing if the result is still ArgumentBinder.BindingResult#UNSATISFIED, it is considered ArgumentBinder.BindingResult#EMPTY. |
Key interfaces are:
AnnotatedRequestArgumentBinder
Argument binders that bind based on the presence of an annotation must implement AnnotatedRequestArgumentBinder, and can be used by creating an annotation that is annotated with @Bindable. For example:
An example of a binding annotation
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.bind.annotation.Bindable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({FIELD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Bindable (1)
public @interface ShoppingCart {
@AliasFor(annotation = Bindable.class, member = "value")
String value() default "";
}
An example of a binding annotation
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.AliasFor
import io.micronaut.core.bind.annotation.Bindable
import java.lang.annotation.Retention
import java.lang.annotation.Target
import static java.lang.annotation.ElementType.ANNOTATION_TYPE
import static java.lang.annotation.ElementType.FIELD
import static java.lang.annotation.ElementType.PARAMETER
import static java.lang.annotation.RetentionPolicy.RUNTIME
@CompileStatic
@Target([FIELD, PARAMETER, ANNOTATION_TYPE])
@Retention(RUNTIME)
@Bindable (1)
@interface ShoppingCart {
@AliasFor(annotation = Bindable, member = "value")
String value() default ""
}
An example of a binding annotation
import io.micronaut.core.bind.annotation.Bindable
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.FIELD
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
@Target(FIELD, VALUE_PARAMETER, ANNOTATION_CLASS)
@Retention(RUNTIME)
@Bindable (1)
annotation class ShoppingCart(val value: String = "")
1 | The binding annotation must itself be annotated as @Bindable |
Example of annotated data binding
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.jackson.serialize.JacksonObjectSerializer;
import jakarta.inject.Singleton;
import java.util.Map;
import java.util.Optional;
@Singleton
public class ShoppingCartRequestArgumentBinder
implements AnnotatedRequestArgumentBinder<ShoppingCart, Object> { (1)
private final ConversionService<?> conversionService;
private final JacksonObjectSerializer objectSerializer;
public ShoppingCartRequestArgumentBinder(ConversionService<?> conversionService,
JacksonObjectSerializer objectSerializer) {
this.conversionService = conversionService;
this.objectSerializer = objectSerializer;
}
@Override
public Class<ShoppingCart> getAnnotationType() {
return ShoppingCart.class;
}
@Override
public BindingResult<Object> bind(
ArgumentConversionContext<Object> context,
HttpRequest<?> source) { (2)
String parameterName = context.getAnnotationMetadata()
.stringValue(ShoppingCart.class)
.orElse(context.getArgument().getName());
Cookie cookie = source.getCookies().get("shoppingCart");
if (cookie == null) {
return BindingResult.EMPTY;
}
Optional<Map<String, Object>> cookieValue = objectSerializer.deserialize(
cookie.getValue().getBytes(),
Argument.mapOf(String.class, Object.class));
return () -> cookieValue.flatMap(map -> {
Object obj = map.get(parameterName);
return conversionService.convert(obj, context);
});
}
}
Example of annotated data binding
import groovy.transform.CompileStatic
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder
import io.micronaut.http.cookie.Cookie
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import jakarta.inject.Singleton
@CompileStatic
@Singleton
class ShoppingCartRequestArgumentBinder
implements AnnotatedRequestArgumentBinder<ShoppingCart, Object> { (1)
private final ConversionService<?> conversionService
private final JacksonObjectSerializer objectSerializer
ShoppingCartRequestArgumentBinder(
ConversionService<?> conversionService,
JacksonObjectSerializer objectSerializer) {
this.conversionService = conversionService
this.objectSerializer = objectSerializer
}
@Override
Class<ShoppingCart> getAnnotationType() {
ShoppingCart
}
@Override
BindingResult<Object> bind(
ArgumentConversionContext<Object> context,
HttpRequest<?> source) { (2)
String parameterName = context.annotationMetadata
.stringValue(ShoppingCart)
.orElse(context.argument.name)
Cookie cookie = source.cookies.get("shoppingCart")
if (!cookie) {
return BindingResult.EMPTY
}
Optional<Map<String, Object>> cookieValue = objectSerializer.deserialize(
cookie.value.bytes,
Argument.mapOf(String, Object))
return (BindingResult) { ->
cookieValue.flatMap({value ->
conversionService.convert(value.get(parameterName), context)
})
}
}
}
Example of annotated data binding
import io.micronaut.core.bind.ArgumentBinder.BindingResult
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import java.util.Optional
import jakarta.inject.Singleton
@Singleton
class ShoppingCartRequestArgumentBinder(
private val conversionService: ConversionService<*>,
private val objectSerializer: JacksonObjectSerializer
) : AnnotatedRequestArgumentBinder<ShoppingCart, Any> { (1)
override fun getAnnotationType(): Class<ShoppingCart> {
return ShoppingCart::class.java
}
override fun bind(context: ArgumentConversionContext<Any>,
source: HttpRequest<*>): BindingResult<Any> { (2)
val parameterName = context.annotationMetadata
.stringValue(ShoppingCart::class.java)
.orElse(context.argument.name)
val cookie = source.cookies.get("shoppingCart") ?: return BindingResult.EMPTY
val cookieValue: Optional<Map<String, Any>> = objectSerializer.deserialize(
cookie.value.toByteArray(),
Argument.mapOf(String::class.java, Any::class.java))
return BindingResult {
cookieValue.flatMap { map: Map<String, Any> ->
conversionService.convert(map[parameterName], context)
}
}
}
}
1 | The custom argument binder must implement AnnotatedRequestArgumentBinder, including both the annotation type to trigger the binder (in this case, MyBindingAnnotation ) and the type of the argument expected (in this case, Object ) |
2 | Override the bind method with the custom argument binding logic - in this case, we resolve the name of the annotated argument, extract a value from a cookie with that same name, and convert that value to the argument type |
It is common to use ConversionService to convert the data to the type of the argument. |
Once the binder is created, we can annotate an argument in our controller method which will be bound using the custom logic we’ve specified.
A controller operation with this annotated binding
@Get("/annotated")
HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { (1)
return HttpResponse.ok("Session:" + sessionId);
}
// end::method
}
A controller operation with this annotated binding
@Get("/annotated")
HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { (1)
HttpResponse.ok("Session:" + sessionId)
}
// end::method
}
A controller operation with this annotated binding
@Get("/annotated")
fun checkSession(@ShoppingCart sessionId: Long): HttpResponse<String> { (1)
return HttpResponse.ok("Session:$sessionId")
}
1 | The parameter is bound with the binder associated with MyBindingAnnotation . This takes precedence over a type-based binder, if applicable. |
TypedRequestArgumentBinder
Argument binders that bind based on the type of the argument must implement TypedRequestArgumentBinder. For example, given this class:
Example of POJO
import io.micronaut.core.annotation.Introspected;
@Introspected
public class ShoppingCart {
private String sessionId;
private Integer total;
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}
}
Example of POJO
import io.micronaut.core.annotation.Introspected
@Introspected
class ShoppingCart {
String sessionId
Integer total
}
Example of POJO
import io.micronaut.core.annotation.Introspected
@Introspected
class ShoppingCart {
var sessionId: String? = null
var total: Int? = null
}
We can define a TypedRequestArgumentBinder
for this class, as seen below:
Example of typed data binding
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.jackson.serialize.JacksonObjectSerializer;
import jakarta.inject.Singleton;
import java.util.Optional;
@Singleton
public class ShoppingCartRequestArgumentBinder
implements TypedRequestArgumentBinder<ShoppingCart> {
private final JacksonObjectSerializer objectSerializer;
public ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
this.objectSerializer = objectSerializer;
}
@Override
public BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context,
HttpRequest<?> source) { (1)
Cookie cookie = source.getCookies().get("shoppingCart");
if (cookie == null) {
return Optional::empty;
}
return () -> objectSerializer.deserialize( (2)
cookie.getValue().getBytes(),
ShoppingCart.class);
}
@Override
public Argument<ShoppingCart> argumentType() {
return Argument.of(ShoppingCart.class); (3)
}
}
Example of typed data binding
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.http.cookie.Cookie
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import jakarta.inject.Singleton
@Singleton
class ShoppingCartRequestArgumentBinder
implements TypedRequestArgumentBinder<ShoppingCart> {
private final JacksonObjectSerializer objectSerializer
ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
this.objectSerializer = objectSerializer
}
@Override
BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context,
HttpRequest<?> source) { (1)
Cookie cookie = source.cookies.get("shoppingCart")
if (!cookie) {
return BindingResult.EMPTY
}
return () -> objectSerializer.deserialize( (2)
cookie.value.bytes,
ShoppingCart)
}
@Override
Argument<ShoppingCart> argumentType() {
Argument.of(ShoppingCart) (3)
}
}
Example of typed data binding
import io.micronaut.core.bind.ArgumentBinder
import io.micronaut.core.bind.ArgumentBinder.BindingResult
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import java.util.Optional
import jakarta.inject.Singleton
@Singleton
class ShoppingCartRequestArgumentBinder(private val objectSerializer: JacksonObjectSerializer) :
TypedRequestArgumentBinder<ShoppingCart> {
override fun bind(
context: ArgumentConversionContext<ShoppingCart>,
source: HttpRequest<*>
): BindingResult<ShoppingCart> { (1)
val cookie = source.cookies["shoppingCart"]
return if (cookie == null)
BindingResult {
Optional.empty()
}
else {
BindingResult {
objectSerializer.deserialize( (2)
cookie.value.toByteArray(),
ShoppingCart::class.java
)
}
}
}
override fun argumentType(): Argument<ShoppingCart> {
return Argument.of(ShoppingCart::class.java) (3)
}
}
1 | Override the bind method with the data type to bind, in this example the ShoppingCart type |
2 | After retrieving the data (in this case, by deserializing JSON text from a cookie), return as a ArgumentBinder.BindingResult |
3 | Also override the argumentType method, which is used by the ArgumentBinderRegistry. |
Once the binder is created, it is used for any controller argument of the associated type:
A controller operation with this typed binding
@Get("/typed")
public HttpResponse<?> loadCart(ShoppingCart shoppingCart) { (1)
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("sessionId", shoppingCart.getSessionId());
responseMap.put("total", shoppingCart.getTotal());
return HttpResponse.ok(responseMap);
}
A controller operation with this typed binding
@Get("/typed")
HttpResponse<Map<String, Object>> loadCart(ShoppingCart shoppingCart) { (1)
HttpResponse.ok(
sessionId: shoppingCart.sessionId,
total: shoppingCart.total)
}
A controller operation with this typed binding
@Get("/typed")
fun loadCart(shoppingCart: ShoppingCart): HttpResponse<*> { (1)
return HttpResponse.ok(mapOf(
"sessionId" to shoppingCart.sessionId,
"total" to shoppingCart.total))
}
1 | The parameter is bound using the custom logic defined for this type in our TypedRequestArgumentBinder |