Generic Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this short tutorial, we'll take a close look at Spring's RestTemplate exception IllegalArgumentException: Not enough variables available to expand.

First, we'll discuss in detail the main cause behind this exception. Then, we'll showcase how to produce it, and finally, how to solve it.

2. The Cause

In short, the exception typically occurs when we're trying to send JSON data in a GET request.

Simply put, RestTemplate provides the getForObject method to get a representation by making a GET request on the specified URL.

The main cause of the exception is that RestTemplate considers the JSON data encapsulated in the curly braces as a placeholder for URI variables.

Since we don't provide any values for the expected URI variables, the getForObject method throws the exception.

For example, attempting to send {“name”:”HP EliteBook”} as value:

String url = "http://products.api.com/get?key=a123456789z&criterion={\"name\":\"HP EliteBook\"}";
Product product = restTemplate.getForObject(url, Product.class);

Will simply cause RestTemplate to throw the exception:

java.lang.IllegalArgumentException: Not enough variable values available to expand 'name'

3. Example Application

Now, let's see an example of how we can produce this IllegalArgumentException using RestTemplate.

To keep things simple, we're going to create a basic REST API for product management with a single GET endpoint.

First, let's create our model class Product:

public class Product {

    private int id;
    private String name;
    private double price;

    // default constructor + all args constructor + getters + setters 
}

Next, we're going to define a spring controller to encapsulate the logic of our REST API:

@RestController
@RequestMapping("/api")
public class ProductApi {

    private List<Product> productList = new ArrayList<>(Arrays.asList(
      new Product(1, "Acer Aspire 5", 437), 
      new Product(2, "ASUS VivoBook", 650), 
      new Product(3, "Lenovo Legion", 990)
    ));

    @GetMapping("/get")
    public Product get(@RequestParam String criterion) throws JsonMappingException, JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        Criterion crt = objectMapper.readValue(criterion, Criterion.class);
        if (crt.getProp().equals("name")) {
            return findByName(crt.getValue());
        }

        // Search by other properties (id,price)

        return null;
    }

    private Product findByName(String name) {
        for (Product product : this.productList) {
            if (product.getName().equals(name)) {
                return product;
            }
        }
        return null;
    }

    // Other methods
}

4. Example Application Explained

The basic idea of the handler method get() is to retrieve a product object based on a specific criterion.

The criterion can be represented as a JSON string with two keys: prop and value.

The prop key refers to a product property, so it can be an id, a name, or a price.

As shown above, the criterion is passed as a string argument to the handler method. We used the ObjectMapper class to convert our JSON string to an object of Criterion.

This is how our Criterion class looks:

public class Criterion {

    private String prop;
    private String value;

    // default constructor + getters + setters
}

Finally, let's try to send a GET request to the URL mapped to the handler method get().

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { RestTemplate.class, RestTemplateExceptionApplication.class })
public class RestTemplateExceptionLiveTest {

    @Autowired
    RestTemplate restTemplate;

    @Test(expected = IllegalArgumentException.class)
    public void givenGetUrl_whenJsonIsPassed_thenThrowException() {
        String url = "http://localhost:8080/spring-rest/api/get?criterion={\"prop\":\"name\",\"value\":\"ASUS VivoBook\"}";
        Product product = restTemplate.getForObject(url, Product.class);
    }
}

Indeed, the unit test throws IllegalArgumentException because we're attempting to pass {“prop”:”name”,”value”:”ASUS VivoBook”} as part of the URL.

5. The Solution

As a rule of thumb, we should always use a POST request to send JSON data.

However, although not recommended, a possible solution using GET could be to define a String object containing our criterion and provide a real URI variable in the URL.

@Test
public void givenGetUrl_whenJsonIsPassed_thenGetProduct() {
    String criterion = "{\"prop\":\"name\",\"value\":\"ASUS VivoBook\"}";
    String url = "http://localhost:8080/spring-rest/api/get?criterion={criterion}";
    Product product = restTemplate.getForObject(url, Product.class, criterion);

    assertEquals(product.getPrice(), 650, 0);
}

Let's look at another solution using the UriComponentsBuilder class:

@Test
public void givenGetUrl_whenJsonIsPassed_thenReturnProduct() {
    String criterion = "{\"prop\":\"name\",\"value\":\"Acer Aspire 5\"}";
    String url = "http://localhost:8080/spring-rest/api/get";

    UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url).queryParam("criterion", criterion);
    Product product = restTemplate.getForObject(builder.build().toUri(), Product.class);

    assertEquals(product.getId(), 1, 0);
}

As we can see, we used the UriComponentsBuilder class to construct our URI with the query parameter criterion before passing it to the getForObject method.

6. Conclusion

In this quick article, we discussed what causes RestTemplate to throw the IllegalArgumentException: “Not enough variables available to expand”.

Along the way, we walked through a practical example showing how to produce the exception and solve it.

As always, the full source code of the examples is available over on GitHub.

Generic bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
guest
0 Comments
Inline Feedbacks
View all comments