DEV Community

Cover image for How to use a custom class as @PathVariable in Feign πŸ‘¨β€πŸ«
Benjamin Rancourt
Benjamin Rancourt

Posted on • Originally published at benjaminrancourt.ca on

How to use a custom class as @PathVariable in Feign πŸ‘¨β€πŸ«

Do you know Feign? It is a Java library that allows you to write a REST client with minimal code. In my current work project, we are currently using it and as we had no experience with it, we are continually improving our knowledge on this library.

We recently encountered a particular use case, that although we could not find an example on the Internet, we were able to find a working solution that you might be interested in. πŸ₯°

Context

We are currently using an external REST API which seems a bit special: for their GET endpoints, we are able to filter the returned ressources with their API, but we cannot pass these filters as normal GET parameters. We have to pass them directly in the URL with a special syntax! 😲

Let's see an example. If we want to filter the /activities endpoint to get the activity with ID 65, we need to call the /activities/id;65 endpoint. A little weird, right? πŸ€”

OR filter

If we want to get two separate activities that have the ID 65 OR 76, the syntax is similar. We must add ;76 to the previous endpoint: /activities/id;65;76.

AND filter

Unfortunately, the filters can be more complicated. If we want to have the activities for 2021 (therefore from January 1 to December 31), we must have an AND filter. And what do you think the syntax looks like? πŸ˜‰

The URL would be /activities/startDate;012F%012F%2021/endDate;122F%312F%2021. I am pretty sure you did not guess it, right? 🀣 The 2F% is the encoded version of a slash (/) character. And yes, the dates must be in the MM/DD/YYYY format... πŸ˜“

Toward the solution

Since we did not want to manually create the filters for all endpoints before calling them, we started looking for a better solution. So we start with a single @GetMapping endpoint:

  @GetMapping("/activities/{filters}")
  List<Activity> getActivities(
      @PathVariable(value = "filters") final String filters
  );
Enter fullscreen mode Exit fullscreen mode
An excerpt of our Feign client, with a String as a @PathVariable

But although the API returns results when we call it from our code, we found that the filters were not applied and the API returned all activities instead of an error... 🀯

Encoding

It turns out that Feign automatically encodes the URL path variable, so the final URI looks like /activities/startDate%3B04%252F01%252F2021 instead of /activities/startDate;012F%012F%2021 that their application was expecting (I removed the endDate filter for clarity).

Thus, semicolons (;) and forward slashes (/) were encoded, as they are generally not expected in a path variable. Fortunately, we quickly found this answer from StackOverflow that steers us on the concept of RequestInterceptor. We were able to decode the encoded values with the code below. 😌

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

/**
 * Interceptor to alter the URL used for the external API. The API uses characters that are encoded by Feign, so
 * this interceptor decode them back to their original values.
 *
 * Inspired from https://stackoverflow.com/a/61901509
 */
@Component
public class PathEncodingExclusionInterceptor implements RequestInterceptor {
  private static final String SEMI_COLON = "%3B";
  private static final String PERCENTAGE = "%25";

  @Override
  public void apply(final RequestTemplate template) {
    final String path = template.path();

    // Make sure to decode the URL only one time, as the uri function can takes only relative URLs
    if (path.contains(SEMI_COLON) || path.contains(PERCENTAGE)) {
      // SOURCE : startDate ; 04 % 2F 01 % 2F 2021 /
      // BEFORE : startDate %3B 04 %25 2F 01 %25 2F 2021 /
      // AFTER : startDate ; 04 % 2F 01 % 2F 2021 /
      template.uri(path.replaceAll(SEMI_COLON, ";").replaceAll(PERCENTAGE, "%"));
    }
  }
}

Enter fullscreen mode Exit fullscreen mode
Our special RequestInterceptor to decode some characters

Now that this issue is resolved, let's get back to creating filters.

Final solution

Use a custom class as @PathVariable

Our original solution was to create a custom builder class that would build a String based on certain values passed as parameters.

But going through the code created by my colleague, I wonder if we could just pass a custom class to Feign as a @PathVariable instead. So we quickly test the following code, where FilterField is our custom class:

  @GetMapping("/activities/{filters}")
  List<Activity> getActivities(
      @PathVariable(value = "filters") final FilterField filters
  );
Enter fullscreen mode Exit fullscreen mode
We replace the type of the @PathVariable from String to FilterField

Let's see the final URL that Feign calls in the output:

/activities/ca.usherbrooke.filter.FilterField%407ab4ae59
Enter fullscreen mode Exit fullscreen mode
The URL, with the class name and something that looks like a hash code...

Overriding the toString function

Interesting, isn't it? This looks like the default implementation of an object's toString function. And what happens when we override it into our class?

package ca.usherbrooke.filter;

import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class FilterField {
  private final String field;
  private final List<String> values;

    @Override
    public String toString() {
      final String joinedValues = String.join(";", values);
      return String.format("%s;%s", field, joinedValues);
    }
}

Enter fullscreen mode Exit fullscreen mode
An excerpt of our FilterField class

You guessed it, our URL looks like exactly as we want! πŸ™‚

/activities/startDate;012F%012F%2021
Enter fullscreen mode Exit fullscreen mode
Much better, right?

Using a List

But since we want to have the possibility to use a list of FilterField, let's update our client t0 allow it:

  @GetMapping("/activities/{filters}")
  List<Activity> getActivities(
      @PathVariable(value = "filters") final List<FilterField> filters
  );
Enter fullscreen mode Exit fullscreen mode
We replace the type of the @PathVariable from FilterField to List<FilterField>
/activities/startDate;012F%012F%2021,endDate;122F%312F%2021
Enter fullscreen mode Exit fullscreen mode
Meh, we have a comma between our filters...

This is almost correct, the separator between the filters should be a slash (/) instead of a comma (,)! Guess how we solved this problem? By creating a new class! πŸ˜„

Creation of a FilterFieldList

First, let's replace our code in our Feign client:

  @GetMapping("/activities/{filters}")
  List<Activity> getActivities(
      @PathVariable(value = "filters") final FilterFieldList filters
  );
Enter fullscreen mode Exit fullscreen mode
We finally replace the type of the @PathVariable from List<FilterField> to FilterFieldList

And now, the implementation of our FilterFieldList class:

package ca.usherbrooke.filter;

import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;

public class FilterFieldList {
  private final List<FilterField> fields;

  public FilterFieldList(final FilterField... filterFields) {
    fields = Arrays.asList(filterFields);
  }

  @Override
  public String toString() {
    final List<String> strings =
        fields.stream().map(FilterField::toString).collect(Collectors.toList());
    return String.join("/", strings);
  }
}

Enter fullscreen mode Exit fullscreen mode
Implementation code of the FilterFieldList class

With this final class, we now have a complete working solution for the weird external API that we are using! πŸ₯³

/activities/startDate;012F%012F%2021/endDate;122F%312F%2021
Enter fullscreen mode Exit fullscreen mode
The final URL, exactly as we want, but without using a String @PathVariable!

We initially try to extend List<FilterField> with the overridden function, but somehow, Feign did not call our toString function... Maybe Feign is doing something different if the custom class is an instance of List? πŸ€”

Conclusion

So, in this post, you learned about two Feign concepts: RequestInterceptor and PathVariable. Hope this would help you as we wish we had found this post before! πŸ˜‡

Note : part of the code displayed was made by my colleague, Alexandre CΓ΄tΓ©, and I could have found this solution without its help. I think that together we managed to find a better solution, so thanks Alex! πŸ˜‰

Top comments (0)