6.5 Custom Argument Binding

Micronaut uses an ArgumentBinderRegistry to look up ArgumentBinders beans capable of binding to the arguments in the controller methods. The default implementation will look for an annotation on the argument that is, itself, annotated with @Bindable. If one exists the argument binder registry will search for an argument binder that supports that annotation.

If no fitting annotation is found Micronaut will try to find an argument binder that supports the argument’s type.

An argument binder returns a 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 the 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 the data is not present, then an UNSATISFIED result should be returned. Returning an 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 UNSATISFIED, it is considered 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.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.Target;
  6. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  7. @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
  8. @Retention(RUNTIME)
  9. @Bindable (1)
  10. public @interface ShoppingCart {
  11. @AliasFor(annotation = Bindable.class, member = "value")
  12. String value() default "";
  13. }

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.ElementType
  5. import java.lang.annotation.Retention
  6. import java.lang.annotation.Target
  7. import static java.lang.annotation.RetentionPolicy.RUNTIME
  8. @CompileStatic
  9. @Target([ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE])
  10. @Retention(RUNTIME)
  11. @Bindable (1)
  12. @interface ShoppingCart {
  13. @AliasFor(annotation = Bindable.class, member = "value")
  14. String value() default ""
  15. }

An example of a binding annotation

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

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

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.*
  9. import javax.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>, source: HttpRequest<*>): BindingResult<Any> { (2)
  19. val parameterName = context.annotationMetadata
  20. .stringValue(ShoppingCart::class.java)
  21. .orElse(context.argument.name)
  22. val cookie = source.cookies.get("shoppingCart")
  23. if (cookie != null) {
  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. return BindingResult.EMPTY
  34. }
  35. }
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 are resolving the name of the annotated argument, extracting a value from a cookie with that same name, and converting that value into 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 binding 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}".toString())
  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 will be bound with the binder associated with MyBindingAnnotation. This will take 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 javax.inject.Singleton;
  8. import java.util.Optional;
  9. @Singleton
  10. public class ShoppingCartRequestArgumentBinder implements TypedRequestArgumentBinder<ShoppingCart> {
  11. private final JacksonObjectSerializer objectSerializer;
  12. public ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
  13. this.objectSerializer = objectSerializer;
  14. }
  15. @Override
  16. public BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context, HttpRequest<?> source) { (1)
  17. Cookie cookie = source.getCookies().get("shoppingCart");
  18. if (cookie != null) {
  19. return () -> objectSerializer.deserialize( (2)
  20. cookie.getValue().getBytes(),
  21. ShoppingCart.class);
  22. }
  23. return Optional::empty;
  24. }
  25. @Override
  26. public Argument<ShoppingCart> argumentType() {
  27. return Argument.of(ShoppingCart.class); (3)
  28. }
  29. }

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 javax.inject.Singleton
  8. @Singleton
  9. class ShoppingCartRequestArgumentBinder implements TypedRequestArgumentBinder<ShoppingCart> {
  10. private final JacksonObjectSerializer objectSerializer
  11. ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
  12. this.objectSerializer = objectSerializer
  13. }
  14. @Override
  15. BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context, HttpRequest<?> source) { (1)
  16. Cookie cookie = source.getCookies().get("shoppingCart")
  17. if (cookie != null) {
  18. return () -> objectSerializer.deserialize( (2)
  19. cookie.getValue().getBytes(),
  20. ShoppingCart)
  21. }
  22. return BindingResult.EMPTY
  23. }
  24. @Override
  25. Argument<ShoppingCart> argumentType() {
  26. Argument.of(ShoppingCart) (3)
  27. }
  28. }

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 javax.inject.Singleton
  9. import java.util.Optional
  10. @Singleton
  11. class ShoppingCartRequestArgumentBinder(private val objectSerializer: JacksonObjectSerializer) :
  12. TypedRequestArgumentBinder<ShoppingCart> {
  13. override fun bind(
  14. context: ArgumentConversionContext<ShoppingCart>,
  15. source: HttpRequest<*>
  16. ): ArgumentBinder.BindingResult<ShoppingCart> { (1)
  17. val cookie = source.cookies["shoppingCart"]
  18. return if (cookie != null) {
  19. BindingResult {
  20. objectSerializer.deserialize( (2)
  21. cookie.value.toByteArray(),
  22. ShoppingCart::class.java
  23. )
  24. }
  25. } else BindingResult {
  26. Optional.empty()
  27. }
  28. }
  29. override fun argumentType(): Argument<ShoppingCart> {
  30. return Argument.of(ShoppingCart::class.java) (3)
  31. }
  32. }
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 BindingResult
3Also override the argumentType method, which is used by the ArgumentBinderRegistry.

Once the binder is created, it will be 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. Map<String, Object> responseMap = [:]
  4. responseMap.sessionId = shoppingCart.sessionId
  5. responseMap.total = shoppingCart.total
  6. return HttpResponse.ok(responseMap)
  7. }

A controller operation with this typed binding

  1. @Get("/typed")
  2. fun loadCart(shoppingCart: ShoppingCart): HttpResponse<*> { (1)
  3. val responseMap: MutableMap<String, Any?> = HashMap()
  4. responseMap["sessionId"] = shoppingCart.sessionId
  5. responseMap["total"] = shoppingCart.total
  6. return HttpResponse.ok<Map<String, Any?>>(responseMap)
  7. }
1The parameter will be bound using the custom logic defined for this type in our TypedRequestArgumentBinder