ruben1691
ruben1691

Reputation: 423

Use JsonView to selectively hide fields

I am trying to selectively hide certain fields in a response, depending on the role of the user requesting it. From what I have been able to understand, JsonView from Jackson may be the way to go.

I need to be able to display all fields, UNLESS they have been marked with a specific access level. I have created the following structure for the access levels:

(Quick note: I have left the base user in there, but it shouldn't really matter)

public class View {
    public static final Map<Role, Class> MAPPING = new HashMap<>();

    static {
        MAPPING.put(Role.ROLE_ADMIN, Admin.class);
        MAPPING.put(Role.ROLE_USER, AuthenticatedUser.class);
    }

    public interface User {}
    public interface AuthenticatedUser extends User {}
    public interface Admin extends AuthenticatedUser {}
}

And used them as follows:

@Entity
public class SomeEntity {
    @Id
    @GeneratedValue
    private Integer id;

    private String baseInfo;

    @JsonView(View.AuthenticatedUser.class)
    private String userInfo;

    @JsonView(View.Admin.class)
    private String secretInfo;

    ...
}

(PS: I have stripped away all the non essential annotations, etc.)

Now, I expect that depending on the level of access, the responses are as follows:

  1. Non-authenticated user:
    {
        "id": 1,
        "baseInfo": "Some basic info"
    } 
    
  2. Authenticated user:
    {
        "id": 1,
        "baseInfo": "Some basic info",
        "userInfo": "Info only the user should be able to see"
    } 
    
  3. Admin user:
    {
        "id": 1,
        "baseInfo": "Some basic info",
        "userInfo": "Info only the user should be able to see",
        "secretInfo": "Info only the admin can see"
    } 
    

I used code from this tutorial to integrate it into spring security and my own structure.

@RestControllerAdvice
class SecurityJsonViewControllerAdvice extends AbstractMappingJacksonResponseBodyAdvice {

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
        if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().getAuthorities() != null) {
            Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
            List<Role> foundRoles = authorities.stream()
                    .map(GrantedAuthority::getAuthority)
                    .map(Role::get).collect(Collectors.toList());

            if (foundRoles.contains(null)) {
                System.err.println("User has no auth. Setting no serialization view");
                return;
            }

            List<Class> jsonViews = foundRoles.stream().map(View.MAPPING::get)
                    .collect(Collectors.toList());
            if (jsonViews.size() == 1) {
                System.err.println("Setting " + jsonViews.get(0) + " as serialization view");
                bodyContainer.setSerializationView(jsonViews.get(0));
                return;
            }
            throw new IllegalArgumentException("Ambiguous @JsonView declaration for roles "
                    + authorities.stream()
                    .map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
        }
        System.err.println("No auth found");
    }
}

(Pardon the awful code, I have been trying a bit of everything...)

At this point, I expect the result to be as the one that I stated above, but I keep on getting all fields, without any kind of filtering. No type of annotation avoids the fields from being serialized. I know I could set the default to be that all fields are excluded EXCEPT for the ones I annotate, but it would mean annotating all fields and the project is quite big.

Is my initial assumption about JsonView wrong, or am I making a mistake somewhere?

Thanks in advance

Upvotes: 3

Views: 1935

Answers (1)

ruben1691
ruben1691

Reputation: 423

Thanks to @second 's comment, I have been able to figure out the issue. As it turns out I made two small mistakes:

1. Setting Jackson to include all properties by default

The first change is setting spring.jackson.mapper.default_view_inclusion to true in your application.properties file. By doing so, Jackson will include all properties by default, removing the issue where the payload is empty

2. Using a "base" user

Whenever an unauthenticated user tries to access the system, we need to set the serialization view to a base user. In my post above, I have the User View but I never used it. Setting this as a default serialization view (in the interceptor above) solves the issue and the result is the one I stated above.

EDIT

I uploaded an example here. It's very simple but it shows how the different fields can be excluded. Use like this:

  • http://localhost:8080/test
    {
        "id":1,
        "baseInfo":"This is the base info"
    }
    
  • http://localhost:8080/test?view=user
    {
        "id":1,
        "baseInfo":"This is the base info",
        "userInfo":"This is the user info"
    }
    
  • http://localhost:8080/test?view=admin
    {
        "id":1,
        "baseInfo":"This is the base info",
        "userInfo": "This is the user info",
        "secretInfo":"This is the secret info"
    }
    

Hope this helps!

Upvotes: 4

Related Questions