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

  1. import io.micronaut.context.annotation.AliasFor;
  2. import io.micronaut.core.bind.annotation.Bindable;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.Target;
  5. import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
  6. import static java.lang.annotation.ElementType.FIELD;
  7. import static java.lang.annotation.ElementType.PARAMETER;
  8. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  9. @Target({FIELD, PARAMETER, ANNOTATION_TYPE})
  10. @Retention(RUNTIME)
  11. @Bindable (1)
  12. public @interface ShoppingCart {
  13. @AliasFor(annotation = Bindable.class, member = "value")
  14. String value() default "";
  15. }

An example of a binding annotation

  1. import groovy.transform.CompileStatic
  2. import io.micronaut.context.annotation.AliasFor
  3. import io.micronaut.core.bind.annotation.Bindable
  4. import java.lang.annotation.Retention
  5. import java.lang.annotation.Target
  6. import static java.lang.annotation.ElementType.ANNOTATION_TYPE
  7. import static java.lang.annotation.ElementType.FIELD
  8. import static java.lang.annotation.ElementType.PARAMETER
  9. import static java.lang.annotation.RetentionPolicy.RUNTIME
  10. @CompileStatic
  11. @Target([FIELD, PARAMETER, ANNOTATION_TYPE])
  12. @Retention(RUNTIME)
  13. @Bindable (1)
  14. @interface ShoppingCart {
  15. @AliasFor(annotation = Bindable, member = "value")
  16. String value() default ""
  17. }

An example of a binding annotation

  1. import io.micronaut.core.bind.annotation.Bindable
  2. import kotlin.annotation.AnnotationRetention.RUNTIME
  3. import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
  4. import kotlin.annotation.AnnotationTarget.FIELD
  5. import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
  6. @Target(FIELD, VALUE_PARAMETER, ANNOTATION_CLASS)
  7. @Retention(RUNTIME)
  8. @Bindable (1)
  9. annotation class ShoppingCart(val value: String = "")
1The binding annotation must itself be annotated as @Bindable

Example of annotated data binding

  1. import io.micronaut.core.convert.ArgumentConversionContext;
  2. import io.micronaut.core.convert.ConversionService;
  3. import io.micronaut.core.type.Argument;
  4. import io.micronaut.http.HttpRequest;
  5. import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder;
  6. import io.micronaut.http.cookie.Cookie;
  7. import io.micronaut.jackson.serialize.JacksonObjectSerializer;
  8. import jakarta.inject.Singleton;
  9. import java.util.Map;
  10. import java.util.Optional;
  11. @Singleton
  12. public class ShoppingCartRequestArgumentBinder
  13. implements AnnotatedRequestArgumentBinder<ShoppingCart, Object> { (1)
  14. private final ConversionService<?> conversionService;
  15. private final JacksonObjectSerializer objectSerializer;
  16. public ShoppingCartRequestArgumentBinder(ConversionService<?> conversionService,
  17. JacksonObjectSerializer objectSerializer) {
  18. this.conversionService = conversionService;
  19. this.objectSerializer = objectSerializer;
  20. }
  21. @Override
  22. public Class<ShoppingCart> getAnnotationType() {
  23. return ShoppingCart.class;
  24. }
  25. @Override
  26. public BindingResult<Object> bind(
  27. ArgumentConversionContext<Object> context,
  28. HttpRequest<?> source) { (2)
  29. String parameterName = context.getAnnotationMetadata()
  30. .stringValue(ShoppingCart.class)
  31. .orElse(context.getArgument().getName());
  32. Cookie cookie = source.getCookies().get("shoppingCart");
  33. if (cookie == null) {
  34. return BindingResult.EMPTY;
  35. }
  36. Optional<Map<String, Object>> cookieValue = objectSerializer.deserialize(
  37. cookie.getValue().getBytes(),
  38. Argument.mapOf(String.class, Object.class));
  39. return () -> cookieValue.flatMap(map -> {
  40. Object obj = map.get(parameterName);
  41. return conversionService.convert(obj, context);
  42. });
  43. }
  44. }

Example of annotated data binding

  1. import groovy.transform.CompileStatic
  2. import io.micronaut.core.convert.ArgumentConversionContext
  3. import io.micronaut.core.convert.ConversionService
  4. import io.micronaut.core.type.Argument
  5. import io.micronaut.http.HttpRequest
  6. import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder
  7. import io.micronaut.http.cookie.Cookie
  8. import io.micronaut.jackson.serialize.JacksonObjectSerializer
  9. import jakarta.inject.Singleton
  10. @CompileStatic
  11. @Singleton
  12. class ShoppingCartRequestArgumentBinder
  13. implements AnnotatedRequestArgumentBinder<ShoppingCart, Object> { (1)
  14. private final ConversionService<?> conversionService
  15. private final JacksonObjectSerializer objectSerializer
  16. ShoppingCartRequestArgumentBinder(
  17. ConversionService<?> conversionService,
  18. JacksonObjectSerializer objectSerializer) {
  19. this.conversionService = conversionService
  20. this.objectSerializer = objectSerializer
  21. }
  22. @Override
  23. Class<ShoppingCart> getAnnotationType() {
  24. ShoppingCart
  25. }
  26. @Override
  27. BindingResult<Object> bind(
  28. ArgumentConversionContext<Object> context,
  29. HttpRequest<?> source) { (2)
  30. String parameterName = context.annotationMetadata
  31. .stringValue(ShoppingCart)
  32. .orElse(context.argument.name)
  33. Cookie cookie = source.cookies.get("shoppingCart")
  34. if (!cookie) {
  35. return BindingResult.EMPTY
  36. }
  37. Optional<Map<String, Object>> cookieValue = objectSerializer.deserialize(
  38. cookie.value.bytes,
  39. Argument.mapOf(String, Object))
  40. return (BindingResult) { ->
  41. cookieValue.flatMap({value ->
  42. conversionService.convert(value.get(parameterName), context)
  43. })
  44. }
  45. }
  46. }

Example of annotated data binding

  1. import io.micronaut.core.bind.ArgumentBinder.BindingResult
  2. import io.micronaut.core.convert.ArgumentConversionContext
  3. import io.micronaut.core.convert.ConversionService
  4. import io.micronaut.core.type.Argument
  5. import io.micronaut.http.HttpRequest
  6. import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder
  7. import io.micronaut.jackson.serialize.JacksonObjectSerializer
  8. import java.util.Optional
  9. import jakarta.inject.Singleton
  10. @Singleton
  11. class ShoppingCartRequestArgumentBinder(
  12. private val conversionService: ConversionService<*>,
  13. private val objectSerializer: JacksonObjectSerializer
  14. ) : AnnotatedRequestArgumentBinder<ShoppingCart, Any> { (1)
  15. override fun getAnnotationType(): Class<ShoppingCart> {
  16. return ShoppingCart::class.java
  17. }
  18. override fun bind(context: ArgumentConversionContext<Any>,
  19. source: HttpRequest<*>): BindingResult<Any> { (2)
  20. val parameterName = context.annotationMetadata
  21. .stringValue(ShoppingCart::class.java)
  22. .orElse(context.argument.name)
  23. val cookie = source.cookies.get("shoppingCart") ?: return BindingResult.EMPTY
  24. val cookieValue: Optional<Map<String, Any>> = objectSerializer.deserialize(
  25. cookie.value.toByteArray(),
  26. Argument.mapOf(String::class.java, Any::class.java))
  27. return BindingResult {
  28. cookieValue.flatMap { map: Map<String, Any> ->
  29. conversionService.convert(map[parameterName], context)
  30. }
  31. }
  32. }
  33. }
1The 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)
2Override 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

  1. @Get("/annotated")
  2. HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { (1)
  3. return HttpResponse.ok("Session:" + sessionId);
  4. }
  5. // end::method
  6. }

A controller operation with this annotated binding

  1. @Get("/annotated")
  2. HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { (1)
  3. HttpResponse.ok("Session:" + sessionId)
  4. }
  5. // end::method
  6. }

A controller operation with this annotated binding

  1. @Get("/annotated")
  2. fun checkSession(@ShoppingCart sessionId: Long): HttpResponse<String> { (1)
  3. return HttpResponse.ok("Session:$sessionId")
  4. }
1The 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

  1. import io.micronaut.core.annotation.Introspected;
  2. @Introspected
  3. public class ShoppingCart {
  4. private String sessionId;
  5. private Integer total;
  6. public String getSessionId() {
  7. return sessionId;
  8. }
  9. public void setSessionId(String sessionId) {
  10. this.sessionId = sessionId;
  11. }
  12. public Integer getTotal() {
  13. return total;
  14. }
  15. public void setTotal(Integer total) {
  16. this.total = total;
  17. }
  18. }

Example of POJO

  1. import io.micronaut.core.annotation.Introspected
  2. @Introspected
  3. class ShoppingCart {
  4. String sessionId
  5. Integer total
  6. }

Example of POJO

  1. import io.micronaut.core.annotation.Introspected
  2. @Introspected
  3. class ShoppingCart {
  4. var sessionId: String? = null
  5. var total: Int? = null
  6. }

We can define a TypedRequestArgumentBinder for this class, as seen below:

Example of typed data binding

  1. import io.micronaut.core.convert.ArgumentConversionContext;
  2. import io.micronaut.core.type.Argument;
  3. import io.micronaut.http.HttpRequest;
  4. import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
  5. import io.micronaut.http.cookie.Cookie;
  6. import io.micronaut.jackson.serialize.JacksonObjectSerializer;
  7. import jakarta.inject.Singleton;
  8. import java.util.Optional;
  9. @Singleton
  10. public class ShoppingCartRequestArgumentBinder
  11. implements TypedRequestArgumentBinder<ShoppingCart> {
  12. private final JacksonObjectSerializer objectSerializer;
  13. public ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
  14. this.objectSerializer = objectSerializer;
  15. }
  16. @Override
  17. public BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context,
  18. HttpRequest<?> source) { (1)
  19. Cookie cookie = source.getCookies().get("shoppingCart");
  20. if (cookie == null) {
  21. return Optional::empty;
  22. }
  23. return () -> objectSerializer.deserialize( (2)
  24. cookie.getValue().getBytes(),
  25. ShoppingCart.class);
  26. }
  27. @Override
  28. public Argument<ShoppingCart> argumentType() {
  29. return Argument.of(ShoppingCart.class); (3)
  30. }
  31. }

Example of typed data binding

  1. import io.micronaut.core.convert.ArgumentConversionContext
  2. import io.micronaut.core.type.Argument
  3. import io.micronaut.http.HttpRequest
  4. import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
  5. import io.micronaut.http.cookie.Cookie
  6. import io.micronaut.jackson.serialize.JacksonObjectSerializer
  7. import jakarta.inject.Singleton
  8. @Singleton
  9. class ShoppingCartRequestArgumentBinder
  10. implements TypedRequestArgumentBinder<ShoppingCart> {
  11. private final JacksonObjectSerializer objectSerializer
  12. ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
  13. this.objectSerializer = objectSerializer
  14. }
  15. @Override
  16. BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context,
  17. HttpRequest<?> source) { (1)
  18. Cookie cookie = source.cookies.get("shoppingCart")
  19. if (!cookie) {
  20. return BindingResult.EMPTY
  21. }
  22. return () -> objectSerializer.deserialize( (2)
  23. cookie.value.bytes,
  24. ShoppingCart)
  25. }
  26. @Override
  27. Argument<ShoppingCart> argumentType() {
  28. Argument.of(ShoppingCart) (3)
  29. }
  30. }

Example of typed data binding

  1. import io.micronaut.core.bind.ArgumentBinder
  2. import io.micronaut.core.bind.ArgumentBinder.BindingResult
  3. import io.micronaut.core.convert.ArgumentConversionContext
  4. import io.micronaut.core.type.Argument
  5. import io.micronaut.http.HttpRequest
  6. import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
  7. import io.micronaut.jackson.serialize.JacksonObjectSerializer
  8. import java.util.Optional
  9. import jakarta.inject.Singleton
  10. @Singleton
  11. class ShoppingCartRequestArgumentBinder(private val objectSerializer: JacksonObjectSerializer) :
  12. TypedRequestArgumentBinder<ShoppingCart> {
  13. override fun bind(
  14. context: ArgumentConversionContext<ShoppingCart>,
  15. source: HttpRequest<*>
  16. ): BindingResult<ShoppingCart> { (1)
  17. val cookie = source.cookies["shoppingCart"]
  18. return if (cookie == null)
  19. BindingResult {
  20. Optional.empty()
  21. }
  22. else {
  23. BindingResult {
  24. objectSerializer.deserialize( (2)
  25. cookie.value.toByteArray(),
  26. ShoppingCart::class.java
  27. )
  28. }
  29. }
  30. }
  31. override fun argumentType(): Argument<ShoppingCart> {
  32. return Argument.of(ShoppingCart::class.java) (3)
  33. }
  34. }
1Override the bind method with the data type to bind, in this example the ShoppingCart type
2After retrieving the data (in this case, by deserializing JSON text from a cookie), return as a ArgumentBinder.BindingResult
3Also 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

  1. @Get("/typed")
  2. public HttpResponse<?> loadCart(ShoppingCart shoppingCart) { (1)
  3. Map<String, Object> responseMap = new HashMap<>();
  4. responseMap.put("sessionId", shoppingCart.getSessionId());
  5. responseMap.put("total", shoppingCart.getTotal());
  6. return HttpResponse.ok(responseMap);
  7. }

A controller operation with this typed binding

  1. @Get("/typed")
  2. HttpResponse<Map<String, Object>> loadCart(ShoppingCart shoppingCart) { (1)
  3. HttpResponse.ok(
  4. sessionId: shoppingCart.sessionId,
  5. total: shoppingCart.total)
  6. }

A controller operation with this typed binding

  1. @Get("/typed")
  2. fun loadCart(shoppingCart: ShoppingCart): HttpResponse<*> { (1)
  3. return HttpResponse.ok(mapOf(
  4. "sessionId" to shoppingCart.sessionId,
  5. "total" to shoppingCart.total))
  6. }
1The parameter is bound using the custom logic defined for this type in our TypedRequestArgumentBinder