Spring Boot controllers: @GetMapping and objects

Daniel Grzelak
5 min readAug 31, 2023

If you have been working with Spring Boot long enough, writing a controller is something you can do in your sleep. Each night you dream about REST. You communicate with your wife through HTTP verbs. Your boss tells you that you need a new endpoint and you write it. Again.

@GetMapping("/pokemon")
public Pokemon getPokemon(@RequestParam Type type,
@RequestParam String name,
@RequestParam(defaultValue = 1) int evolutionStage) {

}

Chances are, it looks somewhat like this.

In this example, it is mighty easy to just pass your parameters to a service and use them. Your logic can look like this:

private final PokemonSearchService pokemonSearchService;

@GetMapping("/pokemon")
public Pokemon getPokemon(@RequestParam Type type,
@RequestParam String name,
@RequestParam(defaultValue = 1) int evolutionStage) {

pokemonSearchService.findPokemon(type, name, evolutionStage);

}

However, sometimes you might need a LOT of parameters.

private final PokemonSearchService pokemonSearchService;

@GetMapping("/pokemon")
public Pokemon getPokemon(@RequestParam Type type,
@RequestParam String name,
@RequestParam(defaultValue = 1) int evolutionStage,
@RequestParam int numberOfLegs,
@RequestParam boolean doesBelongToMisty,
@RequestParam String habitatZone,
@RequestParam Food favouriteFood,
@RequestParam String colour,
@RequestParam boolean includeFakemon) {

}

At this point, it’s kind of hard to just pass the parameters directly downstream. What you often will do, is create a DTO.

private final PokemonSearchService pokemonSearchService;

@GetMapping("/pokemon")
public Pokemon getPokemon(@RequestParam Type type,
@RequestParam String name,
@RequestParam(defaultValue = 1) int evolutionStage,
@RequestParam int numberOfLegs,
@RequestParam boolean doesBelongToMisty,
@RequestParam String habitatZone,
@RequestParam Food favouriteFood,
@RequestParam String colour,
@RequestParam boolean includeFakemon) {

PokemonSearchRequest pokemonSearchRequest = PokemonSearchRequest.builder()
.type(type)
.name(name)
.evolutionStage(evolutionStage)
.numberOfLegs(numberOfLegs)
.doesBelongToMisty(doesBelongToMisty)
.habitatZone(habitatZone)
.favouriteFood(favouriteFood)
.colour(colour)
.includeFakemon(includeFakemon)
.build();
pokemonSearchService.findPokemon(pokemonSearchRequest);

}

Wow, that’s a lot of writing! And sometimes, you will have way more complex examples than the one above. Additionally, you might say that controller is not a place to construct your object. Is there a different way? Yes, there is.

I decided to write this post as somewhat comprehensive guide to using DTOs in Spring Boot @GetMapping since the information presented is not, at least in my experience, easily found.

In a standard POST mapping, you are able to specify a @RequestBody.



@PostMapping("/pokemon")
public Pokemon addPokemon(@RequestBody PokemonAddRequest pokemonAddRequest) {

pokemonSearchService.addPokemon(pokemonAddRequest);

}

It turns out, you can write your @GetMapping in the same way, albeit without the @RequestBody annotation.

@GetMapping("/pokemon")
public Pokemon getPokemon(PokemonSearchRequest pokemonSearchRequest) {

pokemonSearchService.findPokemon(pokemonSearchRequest);

}

That’s way less verbose! Let’s explore that further, assuming that your DTO for the brevity sake looks like this:

public class PokemonSearchRequest {

private final String name;
private final String habitatZone;
private final int evolutionStage;

}

Validation

Normally, you would put the validation on the Controller level, like below:

@GetMapping("/pokemon")
public Pokemon getPokemon(@RequestParam Type type,
@RequestParam @NonEmpty String name,
@RequestParam @Min(1) int evolutionStage) {

}

In this case, you can just put all of your validation in your DTO, assuming you mark it as @Valid in your controller method.

@GetMapping("/pokemon")
public Pokemon getPokemon(@Valid PokemonSearchRequest pokemonSearchRequest) {

}
public class PokemonSearchRequest {

@NonEmpty
private final String name;
private final String habitatZone;
@Min(1)
private final int evolutionStage;

}

All the rules apply the same as you would validate on the Controller level, but now you validation of multiple fields becomes easier, as you can, for example, make a class level validation.

@ValidSearchRequest
public class PokemonSearchRequest {

@NonEmpty
private final String name;
private final String habitatZone;
@Min(1)
private final int evolutionStage;

}

Swagger

If you have worked with Swagger, I do not need to tell you how much verbosity it adds to the controller. Maybe you extract it to a separate interface. But, just maybe, you want to put it all in your DTO. Now, you can.

Instead of having an incredibly verbose:

@GetMapping("/pokemon")
@ApiOperation(value = "Get Pokemon Details",
notes = "Get details of a Pokemon based on various parameters.")
public Pokemon getPokemon(
@ApiParam(name = "type", value = "Type of the Pokemon", required = true)
@RequestParam Type type,

@ApiParam(name = "name", value = "Name of the Pokemon", required = true)
@RequestParam String name,

@ApiParam(name = "evolutionStage", value = "Evolution stage of the Pokemon", defaultValue = "1")
@RequestParam int evolutionStage,

@ApiParam(name = "numberOfLegs", value = "Number of legs the Pokemon has", required = true)
@RequestParam int numberOfLegs,

@ApiParam(name = "doesBelongToMisty", value = "Whether the Pokemon belongs to Misty", required = true)
@RequestParam boolean doesBelongToMisty,

@ApiParam(name = "habitatZone", value = "Habitat zone of the Pokemon", required = true)
@RequestParam String habitatZone,

@ApiParam(name = "favouriteFood", value = "Favorite food of the Pokemon", required = true)
@RequestParam Food favouriteFood,

@ApiParam(name = "colour", value = "Color of the Pokemon", required = true)
@RequestParam String colour,

@ApiParam(name = "includeFakemon", value = "Include Fakemon in the result")
@RequestParam boolean includeFakemon) {

PokemonSearchRequest pokemonSearchRequest = PokemonSearchRequest.builder()
.type(type)
.name(name)
.evolutionStage(evolutionStage)
.numberOfLegs(numberOfLegs)
.doesBelongToMisty(doesBelongToMisty)
.habitatZone(habitatZone)
.favouriteFood(favouriteFood)
.colour(colour)
.includeFakemon(includeFakemon)
.build();

return pokemonSearchService.findPokemon(pokemonSearchRequest);
}

You can just mark in your controller that your DTO is to be used as a Swagger documentation with @ParameterObject annotation:

@GetMapping("/pokemon")
public Pokemon getPokemon(@ParameterObject PokemonSearchRequest pokemonSearchRequest) {

}

And make the actual annotations on the DTO itself, which frees your controller of verbose documentation. Now you have to deal with it somewhere else, but it’s a choice you can make.

@ApiModel(description = "Parameters to search for a Pokemon")
public class PokemonSearchRequest {

@Parameter(description = "Type of the Pokemon", required = true)
private Type type;

@Parameter(description = "Name of the Pokemon", required = true)
private String name;

@Parameter(description = "Evolution stage of the Pokemon", example = "1")
private int evolutionStage;

@Parameter(description = "Number of legs the Pokemon has", required = true)
private int numberOfLegs;

@Parameter(description = "Whether the Pokemon belongs to Misty", required = true)
private boolean doesBelongToMisty;

@Parameter(description = "Habitat zone of the Pokemon", required = true)
private String habitatZone;

@Parameter(description = "Favorite food of the Pokemon", required = true)
private Food favouriteFood;

@Parameter(description = "Color of the Pokemon", required = true)
private String colour;

@Parameter(description = "Include Fakemon in the result")
private boolean includeFakemon;

}

Jackson, ObjectMapper

Unfortunately, Jackson annotations will not work for you in this case. Unlike when you use @RequestBody annotation for POSTs, Spring does the mapping using setters or constructors in your class. But that brings different kind of possibilities.

Let’s assume that you already had an endpoint, where the `food` was just plain string.

@GetMapping("/pokemon")
public Pokemon getPokemon(String food) {

}

You do not want to break compatibility, but you really, really want to change type of `food` param in order to make your life easier. You can achieve that. Just use a setter, or do it in a constructor.


public class PokemonSearchRequest {

private final Food food;

public void setFood(String food) {
this.food = // someComplicatedLogicWithYourFoodString
}

}

You do not even need Jackson annotations here, it works automagically.

Summary

We learned how to use DTOs in @GetMapping, how to manipulate the request parameters and how to make our DTOs work with Swagger.

Hopefully, I brought you all the information you need so that you can create your GET endpoint just as you dreamed it.

--

--