Dynamic filtering of a field in the response from a RESTful webservice enpoint that returns a List of domain objects

Viewed 3830

Given a RESTful web service developed using the Spring Boot framework, I wanted a way to suppress the birthDate of all Users in the response. This is what I implemented after looking around for a solution :

@RestController
public class UserResource {

    @Autowired
    private UserDAOservice userDAOService;

    @GetMapping("/users")
    public MappingJacksonValue users() {
        List<User> users = userDAOService.findAll();

        SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
                .filterOutAllExcept("id", "name");

        FilterProvider filters = new SimpleFilterProvider().addFilter(
                "UserBirthDateFilter", filter);

        MappingJacksonValue mapping = new MappingJacksonValue(users);

        mapping.setFilters(filters);

        return mapping;
    }
}

However, when I hit the rest end point in the browser, I can still see the birth date of the user in the response :

{
    "id": 1,
    "name": "Adam",
    "birthDate": "1980-03-31T16:56:28.926+0000"
}

Question 1 : What API can I use to achieve my objective?

Next, assuming that I want to adhere to HATEOAS in combination with filtering, how can I go about doing this. I am unable to figure out the APIs that can be used for using these two features together :

@GetMapping("/users/{id}")
public EntityModel<User> users(@PathVariable Integer id) {
    User user = userDAOService.findById(id);
    if (user == null) {
        throw new ResourceNotFoundException("id-" + id);
    }

    EntityModel<User> model = new EntityModel<>(user);
    WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).users());
    model.add(linkTo.withRel("all-users"));

    //how do I combine EntityModel with filtering?
    return model;
}

Question 2 : How do I combine EntityModel with MappingJacksonValue?

Note : I am aware of @JsonIgnore annotation but that would apply the filter for all end points that use the domain; however, I want to restrict the filtering only to the two endpoints above.

4 Answers

Turns out for this to work, I have to add the @JsonFilter annotation above the DTO and provide the same name that was used while creating the SimpleFilterProvider.

@JsonFilter("UserBirthDateFilter")
public class User {

    private Integer id;


    @Size(min=2, message="user name must be atleast 2 characters")
    @ApiModelProperty(notes="user name must be atleast 2 characters")
    private String name;

    @Past
    @ApiModelProperty(notes="birth date cannot be in the past")
    private Date birthDate;

   //other methods
}

There is an easier way to do this, on your transfer object (the class you are sending back to the client), you can simply use the @JsonIgnore annotation to make sure the field is not serialized, and therefore sent to the client. So simply add @JsonIgnore inside your User class for your birthDay field.

You can also read more here about this approach:

https://www.baeldung.com/jackson-ignore-properties-on-serialization

If you need to return a different object for different endpoints (User without birthDay in your case, only for specific) you should create separate transfer objects and use those for their respective endpoints. You can pass your original entity (User) in the constructor to those classes and copy over all fields needed.

You can use Jackson's @JsonView feature. With this, you can tell a certain request mapping to produce serialized JSON with chosen set of properties.

public class View {
    interface UserDetails {}
}

public class User {

    @JsonView(View.UserDetails.class)
    private Long id;

    @JsonView(View.UserDetails.class)
    private String name;

    private String birthdate;
}

Controller be like

    @JsonView(View.UserDetails.class)
    @GetMapping("/users")
    public MappingJacksonValue users() {
    ....
    }

For question 2, I had the exact same question as you did, and here's what I did. It seems to be working:

@GetMapping(path = "/users/{id}")
public MappingJacksonValue retrieveUser(@PathVariable int id){
    User user = service.findOne(id);
    if(user==null){
        throw new UserNotFoundException("id-"+id);
    }
    //"all-users", SERVER_PATH + "/users"
    EntityModel<User> resource = EntityModel.of(user);
    WebMvcLinkBuilder linkTo =
            linkTo(methodOn(this.getClass()).retrieveAllUsers());
    resource.add(linkTo.withRel("all-users"));

    SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("id");
    FilterProvider filters = new SimpleFilterProvider().addFilter("UserFilter",filter);
    MappingJacksonValue mapping = new MappingJacksonValue(resource);
    mapping.setFilters(filters);
    return mapping;
}

Response for HTTP GET localhost:8080/users/1

{
"id": 1,
"links": [
    {
        "rel": "all-users",
        "href": "http://localhost:8080/users"
    }
]}
Related