How to use one DTO for different json REST service with Jackson @JsonView: Or one ring to rule them all
Disclaimer: This story is based on real events. All names of characters and subject matter have been altered to avoid any connection with reality. Due to the widespread use of the Spring Boot framework in my work experience, this story will be dedicated to implementing @JsonView in a REST service using the Spring Boot framework. However, this experience can also be implemented in other frameworks or without them (using the mentioned libraries), but it may not be relevant.
— Hi Bill, I’ve got a task where I need to make some edits to the car dealership service that you wrote, and I have some questions.
— What questions, Jane?
— I need to add the output of car dealership coordinates in the selection. But I found about 20 DTOs for the car dealership. Which one do I need to solve the task?
— I don’t know, Jane, I wrote that service a long time ago. Probably all of them, or at least some of them.
— But how did it happen, Bill? Couldn’t there be just one DTO for one entity?
— Jane, you don’t understand: the car dealership entity consists of more than 50 fields, and the service was supposed to return only some of them in different methods.
— So, we need to have many DTOs just so that the service can return different representations of the same entity without additional logics?
— Yes, Jane
— Our service is written in Spring-Boot and uses Jackson for JSON serialization/deserialization, right, Bill?
— Yes, you are absolutely right.
— Then I have a suggestion on how we can implement your idea with just one DTO.
— That sounds very interesting, Jane, tell me your idea.
— We will need to perform a few simple steps:
1. Write a DTO for the car dealership entity with all fields.
2. Write a view using an interface or class.
3. Apply the view to the DTO using the @JsonView annotation.
4. Apply the corresponding views to the controller methods using the @JsonView annotation.
— Jane, I think I understand you, but I would like to go through this with an example.
— Sure, I wrote a simple project with samples for this case, albeit with a different domain and fewer fields. But it should suffice for an example. Now let’s go through these steps with an example.
— Let’s do it, Jane.
— First, let’s define views for three methods, each corresponding to a representation of the Character
entity with a different set of attributes: title of character, characteristics (height, weight, age) of character, and all fields of character.
public interface CharacterView {
interface Title {}
interface Characteristics {}
interface Character extends Title, Characteristics {}
}
— What’s next, Jane?
— Next, we’ll apply these views to the corresponding fields of the CharacterDetail
DTO.
public class CharacterDetail {
@JsonView({
CharacterView.Title.class,
CharacterView.Characteristics.class
})
int id;
@JsonView(CharacterView.Title.class)
String name;
@JsonView(CharacterView.Title.class)
String secondName;
@JsonView(CharacterView.Characteristics.class)
int age;
@JsonView(CharacterView.Characteristics.class)
int height;
@JsonView(CharacterView.Characteristics.class)
double weight;
}
— This is getting interesting, Jane.
— The final step is to implement a controller with three methods to return different representations and apply our views to them.
@RequestMapping("character")
@RestController
public class CharacterController {
CharacterService characterService;
@JsonView(CharacterView.Character.class)
@GetMapping
public Collection<CharacterDetail> getAllCharacters() {
return characterService.getAll();
}
@GetMapping("/title")
@JsonView(CharacterView.Title.class)
public Collection<CharacterDetail> getAllTitles() {
return characterService.getAll();
}
@GetMapping("/characteristic")
@JsonView(CharacterView.Characteristics.class)
public Collection<CharacterDetail> getAllCharacteristics() {
return characterService.getAll();
}
}
— Now, if we run our service from the example and call different methods, we will get different representations, respectively:
1. http://localhost:8080/character
[
{
"id": 1,
"name": "Alan",
"secondName": "Sleep",
"age": 23,
"height": 173,
"weight": 80.2
},
{
"id": 2,
"name": "Curious",
"secondName": "Sam",
"age": 30,
"height": 190,
"weight": 100.0
},
{
"id": 3,
"name": "Duke",
"secondName": "McKen",
"age": 100,
"height": 200,
"weight": 120.0
},
{
"id": 4,
"name": "Max",
"secondName": "Painkiller",
"age": 33,
"height": 173,
"weight": 85.7
}
]
2. http://localhost:8080/character/title
[
{
"id": 1,
"name": "Alan",
"secondName": "Sleep"
},
{
"id": 2,
"name": "Curious",
"secondName": "Sam"
},
"id": 3,
"name": "Duke",
"secondName": "McKen"
},
{
"id": 4,
"name": "Max",
"secondName": "Painkiller"
}
]
3. http://localhost:8080/character/characteristic
[
{
"id": 1,
"age": 23,
"height": 173,
"weight": 80.2
},
{
"id": 2,
"age": 30,
"height": 190,
"weight": 100.0
},
{
"id": 3,
"age": 100,
"height": 200,
"weight": 120.0
},
{
"id": 4,
"age": 33,
"height": 173,
"weight": 85.7
}
]
— Jane, this is exactly what we need to eliminate unnecessary duplication of DTOs in the car dealership service and improve the service’s scalability. Let’s do it, Jane!
— Yes, Bill, let’s do it! Good luck!