7.3.1 Customizing Parameter Binding

The previous example presented a simple example using method parameters to represent the body of a POST request:

PetOperations.java

  1. @Post
  2. @SingleResult
  3. Publisher<Pet> save(@NotBlank String name, @Min(1L) int age);

The save method performs an HTTP POST with the following JSON by default:

Example Produced JSON

  1. {"name":"Dino", "age":10}

You may however want to customize what is sent as the body, the parameters, URI variables, etc. The @Client annotation is very flexible in this regard and supports the same HTTP Annotations as Micronaut’s HTTP server.

For example, the following defines a URI template, and the name parameter is used as part of the URI template, whilst @Body declares that the contents to send to the server are represented by the Pet POJO:

PetOperations.java

  1. @Post("/{name}")
  2. Mono<Pet> save(
  3. @NotBlank String name, (1)
  4. @Body @Valid Pet pet) (2)
1The name parameter, included as part of the URI, and declared @NotBlank
2The pet parameter, used to encode the body and declared @Valid

The following table summarizes the parameter annotations and their purpose, and provides an example:

Table 1. Parameter Binding Annotations
AnnotationDescriptionExample

@Body

Specifies the parameter for the body of the request

@Body String body

@CookieValue

Specifies parameters to be sent as cookies

@CookieValue String myCookie

@Header

Specifies parameters to be sent as HTTP headers

@Header String requestId

@QueryValue

Customizes the name of the URI parameter to bind from

@QueryValue(“userAge”) Integer age

@PathVariable

Binds a parameter exclusively from a Path Variable.

@PathVariable Long id

@RequestAttribute

Specifies parameters to be set as request attributes

@RequestAttribute Integer locationId

Always use @Produces or @Consumes instead of supplying a header for Content-Type or Accept.

Type-Based Binding Parameters

Some parameters are recognized by their type instead of their annotation. The following table summarizes these parameter types and their purpose, and provides an example:

TypeDescriptionExample

BasicAuth

Binds Basic Authorization credentials

BasicAuth basicAuth

Custom Binding

The ClientArgumentRequestBinder API binds client arguments to the request. Custom binder classes registered as beans are automatically used during the binding process. Annotation-based binders are searched for first, with type-based binders being searched if a binder was not found.

This is an experimental feature in 2.1 and subject to change! One limitation of binding is that binders may not manipulate or set the request URI because it can be a combination of many arguments.
Binding By Annotation

To control how an argument is bound to the request based on an annotation on the argument, create a bean of type AnnotatedClientArgumentRequestBinder. Any custom annotations must be annotated with @Bindable.

In this example, see the following client:

Client With @Metadata Argument

  1. @Client("/")
  2. public interface MetadataClient {
  3. @Get("/client/bind")
  4. String get(@Metadata Map<String, Object> metadata);
  5. }

Client With @Metadata Argument

  1. @Client("/")
  2. interface MetadataClient {
  3. @Get("/client/bind")
  4. String get(@Metadata Map metadata)
  5. }

Client With @Metadata Argument

  1. @Client("/")
  2. interface MetadataClient {
  3. @Get("/client/bind")
  4. operator fun get(@Metadata metadata: Map<String, Any>): String
  5. }

The argument is annotated with the following annotation:

@Metadata Annotation

  1. import io.micronaut.core.bind.annotation.Bindable;
  2. import java.lang.annotation.Documented;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.Target;
  5. import static java.lang.annotation.ElementType.PARAMETER;
  6. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  7. @Documented
  8. @Retention(RUNTIME)
  9. @Target(PARAMETER)
  10. @Bindable
  11. public @interface Metadata {
  12. }

@Metadata Annotation

  1. import io.micronaut.core.bind.annotation.Bindable
  2. import java.lang.annotation.Documented
  3. import java.lang.annotation.Retention
  4. import java.lang.annotation.Target
  5. import static java.lang.annotation.ElementType.PARAMETER
  6. import static java.lang.annotation.RetentionPolicy.RUNTIME
  7. @Documented
  8. @Retention(RUNTIME)
  9. @Target(PARAMETER)
  10. @Bindable
  11. @interface Metadata {
  12. }

@Metadata Annotation

  1. import io.micronaut.core.bind.annotation.Bindable
  2. import kotlin.annotation.AnnotationRetention.RUNTIME
  3. import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
  4. @MustBeDocumented
  5. @Retention(RUNTIME)
  6. @Target(VALUE_PARAMETER)
  7. @Bindable
  8. annotation class Metadata

Without any additional code, the client attempts to convert the metadata to a string and append it as a query parameter. In this case that isn’t the desired effect, so a custom binder is needed.

The following binder handles arguments passed to clients that are annotated with the @Metadata annotation, and mutate the request to contain the desired headers. The implementation could be modified to accept more types of data other than Map.

Annotation Argument Binder

  1. import io.micronaut.core.annotation.NonNull;
  2. import io.micronaut.core.convert.ArgumentConversionContext;
  3. import io.micronaut.core.naming.NameUtils;
  4. import io.micronaut.core.util.StringUtils;
  5. import io.micronaut.http.MutableHttpRequest;
  6. import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder;
  7. import io.micronaut.http.client.bind.ClientRequestUriContext;
  8. import org.jetbrains.annotations.NotNull;
  9. import jakarta.inject.Singleton;
  10. import java.util.Map;
  11. @Singleton
  12. public class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {
  13. @NotNull
  14. @Override
  15. public Class<Metadata> getAnnotationType() {
  16. return Metadata.class;
  17. }
  18. @Override
  19. public void bind(@NotNull ArgumentConversionContext<Object> context,
  20. @NonNull ClientRequestUriContext uriContext,
  21. @NotNull Object value,
  22. @NotNull MutableHttpRequest<?> request) {
  23. if (value instanceof Map) {
  24. for (Map.Entry<?, ?> entry: ((Map<?, ?>) value).entrySet()) {
  25. String key = NameUtils.hyphenate(StringUtils.capitalize(entry.getKey().toString()), false);
  26. request.header("X-Metadata-" + key, entry.getValue().toString());
  27. }
  28. }
  29. }
  30. }

Annotation Argument Binder

  1. import io.micronaut.core.annotation.NonNull
  2. import io.micronaut.core.convert.ArgumentConversionContext
  3. import io.micronaut.core.naming.NameUtils
  4. import io.micronaut.core.util.StringUtils
  5. import io.micronaut.http.MutableHttpRequest
  6. import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder
  7. import io.micronaut.http.client.bind.ClientRequestUriContext
  8. import org.jetbrains.annotations.NotNull
  9. import jakarta.inject.Singleton
  10. @Singleton
  11. class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {
  12. final Class<Metadata> annotationType = Metadata
  13. @Override
  14. void bind(@NotNull ArgumentConversionContext<Object> context,
  15. @NonNull ClientRequestUriContext uriContext,
  16. @NotNull Object value,
  17. @NotNull MutableHttpRequest<?> request) {
  18. if (value instanceof Map) {
  19. for (entry in value.entrySet()) {
  20. String key = NameUtils.hyphenate(StringUtils.capitalize(entry.key as String), false)
  21. request.header("X-Metadata-$key", entry.value as String)
  22. }
  23. }
  24. }
  25. }

Annotation Argument Binder

  1. import io.micronaut.core.convert.ArgumentConversionContext
  2. import io.micronaut.core.naming.NameUtils
  3. import io.micronaut.core.util.StringUtils
  4. import io.micronaut.http.MutableHttpRequest
  5. import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder
  6. import io.micronaut.http.client.bind.ClientRequestUriContext
  7. import jakarta.inject.Singleton
  8. @Singleton
  9. class MetadataClientArgumentBinder : AnnotatedClientArgumentRequestBinder<Metadata> {
  10. override fun getAnnotationType(): Class<Metadata> {
  11. return Metadata::class.java
  12. }
  13. override fun bind(context: ArgumentConversionContext<Any>,
  14. uriContext: ClientRequestUriContext,
  15. value: Any,
  16. request: MutableHttpRequest<*>) {
  17. if (value is Map<*, *>) {
  18. for ((key1, value1) in value) {
  19. val key = NameUtils.hyphenate(StringUtils.capitalize(key1.toString()), false)
  20. request.header("X-Metadata-$key", value1.toString())
  21. }
  22. }
  23. }
  24. }
Binding By Type

To bind to the request based on the type of the argument, create a bean of type TypedClientArgumentRequestBinder.

In this example, see the following client:

Client With Metadata Argument

  1. @Client("/")
  2. public interface MetadataClient {
  3. @Get("/client/bind")
  4. String get(Metadata metadata);
  5. }

Client With Metadata Argument

  1. @Client("/")
  2. interface MetadataClient {
  3. @Get("/client/bind")
  4. String get(Metadata metadata)
  5. }

Client With Metadata Argument

  1. @Client("/")
  2. interface MetadataClient {
  3. @Get("/client/bind")
  4. operator fun get(metadata: Metadata?): String?
  5. }

Without any additional code, the client attempts to convert the metadata to a string and append it as a query parameter. In this case that isn’t the desired effect, so a custom binder is needed.

The following binder handles arguments passed to clients of type Metadata and mutate the request to contain the desired headers.

Typed Argument Binder

  1. import io.micronaut.core.annotation.NonNull;
  2. import io.micronaut.core.convert.ArgumentConversionContext;
  3. import io.micronaut.core.type.Argument;
  4. import io.micronaut.http.MutableHttpRequest;
  5. import io.micronaut.http.client.bind.ClientRequestUriContext;
  6. import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder;
  7. import jakarta.inject.Singleton;
  8. @Singleton
  9. public class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {
  10. @Override
  11. @NonNull
  12. public Argument<Metadata> argumentType() {
  13. return Argument.of(Metadata.class);
  14. }
  15. @Override
  16. public void bind(@NonNull ArgumentConversionContext<Metadata> context,
  17. @NonNull ClientRequestUriContext uriContext,
  18. @NonNull Metadata value,
  19. @NonNull MutableHttpRequest<?> request) {
  20. request.header("X-Metadata-Version", value.getVersion().toString());
  21. request.header("X-Metadata-Deployment-Id", value.getDeploymentId().toString());
  22. }
  23. }

Typed Argument Binder

  1. import io.micronaut.core.annotation.NonNull
  2. import io.micronaut.core.convert.ArgumentConversionContext
  3. import io.micronaut.core.type.Argument
  4. import io.micronaut.http.MutableHttpRequest
  5. import io.micronaut.http.client.bind.ClientRequestUriContext
  6. import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder
  7. import jakarta.inject.Singleton
  8. @Singleton
  9. class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {
  10. @Override
  11. @NonNull
  12. Argument<Metadata> argumentType() {
  13. Argument.of(Metadata)
  14. }
  15. @Override
  16. void bind(@NonNull ArgumentConversionContext<Metadata> context,
  17. @NonNull ClientRequestUriContext uriContext,
  18. @NonNull Metadata value,
  19. @NonNull MutableHttpRequest<?> request) {
  20. request.header("X-Metadata-Version", value.version.toString())
  21. request.header("X-Metadata-Deployment-Id", value.deploymentId.toString())
  22. }
  23. }

Typed Argument Binder

  1. import io.micronaut.core.convert.ArgumentConversionContext
  2. import io.micronaut.core.type.Argument
  3. import io.micronaut.http.MutableHttpRequest
  4. import io.micronaut.http.client.bind.ClientRequestUriContext
  5. import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder
  6. import jakarta.inject.Singleton
  7. @Singleton
  8. class MetadataClientArgumentBinder : TypedClientArgumentRequestBinder<Metadata> {
  9. override fun argumentType(): Argument<Metadata> {
  10. return Argument.of(Metadata::class.java)
  11. }
  12. override fun bind(
  13. context: ArgumentConversionContext<Metadata>,
  14. uriContext: ClientRequestUriContext,
  15. value: Metadata,
  16. request: MutableHttpRequest<*>
  17. ) {
  18. request.header("X-Metadata-Version", value.version.toString())
  19. request.header("X-Metadata-Deployment-Id", value.deploymentId.toString())
  20. }
  21. }

Binding On Method

It is also possible to create a binder, that would change the request with an annotation on the method. For example:

Client With Annotated Method

  1. @Client("/")
  2. public interface NameAuthorizedClient {
  3. @Get("/client/authorized-resource")
  4. @NameAuthorization(name="Bob") (1)
  5. String get();
  6. }

Client With Annotated Method

  1. @Client("/")
  2. public interface NameAuthorizedClient {
  3. @Get("/client/authorized-resource")
  4. @NameAuthorization(name="Bob") (1)
  5. String get()
  6. }

Client With Annotated Method

  1. @Client("/")
  2. public interface NameAuthorizedClient {
  3. @Get("/client/authorized-resource")
  4. @NameAuthorization(name="Bob") (1)
  5. fun get(): String
  6. }
1The @NameAuthorization is annotating a method

The annotation is defined as:

Annotation Definition

  1. @Documented
  2. @Retention(RUNTIME)
  3. @Target(METHOD) (1)
  4. @Bindable
  5. public @interface NameAuthorization {
  6. @AliasFor(member = "name")
  7. String value() default "";
  8. @AliasFor(member = "value")
  9. String name() default "";
  10. }

Annotation Definition

  1. @Documented
  2. @Retention(RUNTIME)
  3. @Target(METHOD) (1)
  4. @Bindable
  5. @interface NameAuthorization {
  6. @AliasFor(member = "name")
  7. String value() default ""
  8. @AliasFor(member = "value")
  9. String name() default ""
  10. }

Annotation Definition

  1. @MustBeDocumented
  2. @Retention(RUNTIME)
  3. @Target(FUNCTION) (1)
  4. @Bindable
  5. annotation class NameAuthorization(val name: String = "")
1It is defined to be used on methods

The following binder specifies the behaviour:

Annotation Definition

  1. @Singleton (1)
  2. public class NameAuthorizationBinder implements AnnotatedClientRequestBinder<NameAuthorization> { (2)
  3. @NotNull
  4. @Override
  5. public Class<NameAuthorization> getAnnotationType() {
  6. return NameAuthorization.class;
  7. }
  8. @Override
  9. public void bind( (3)
  10. @NonNull MethodInvocationContext<Object, Object> context,
  11. @NonNull ClientRequestUriContext uriContext,
  12. @NonNull MutableHttpRequest<?> request
  13. ) {
  14. context.getValue(NameAuthorization.class)
  15. .ifPresent(name -> uriContext.addQueryParameter("name", String.valueOf(name)));
  16. }
  17. }

Annotation Definition

  1. @Singleton (1)
  2. public class NameAuthorizationBinder implements AnnotatedClientRequestBinder<NameAuthorization> { (2)
  3. @NotNull
  4. @Override
  5. Class<NameAuthorization> getAnnotationType() {
  6. return NameAuthorization.class
  7. }
  8. @Override
  9. void bind( (3)
  10. @NonNull MethodInvocationContext<Object, Object> context,
  11. @NonNull ClientRequestUriContext uriContext,
  12. @NonNull MutableHttpRequest<?> request
  13. ) {
  14. context.getValue(NameAuthorization.class)
  15. .ifPresent(name -> uriContext.addQueryParameter("name", String.valueOf(name)))
  16. }
  17. }

Annotation Definition

  1. import io.micronaut.http.client.bind.AnnotatedClientRequestBinder
  2. @Singleton (1)
  3. class NameAuthorizationBinder: AnnotatedClientRequestBinder<NameAuthorization> { (2)
  4. @NotNull
  5. override fun getAnnotationType(): Class<NameAuthorization> {
  6. return NameAuthorization::class.java
  7. }
  8. override fun bind( (3)
  9. @NonNull context: MethodInvocationContext<Any, Any>,
  10. @NonNull uriContext: ClientRequestUriContext,
  11. @NonNull request: MutableHttpRequest<*>
  12. ) {
  13. context.getValue(NameAuthorization::class.java, "name")
  14. .ifPresent { name -> uriContext.addQueryParameter("name", name.toString()) }
  15. }
  16. }
1The @Singleton annotation registers it in Micronaut context
2It implements the AnnotatedClientRequestBinder<NameAuthorization>
3The custom bind method is used to implement the behaviour of the binder