Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Spring MVC @JsonView when returning an object hierarchy from a Rest Controller

I'm building an application which uses Spring MVC 4.10 with jackson 2.3.2. I have a Project class which has children Proposal objects and a Customer object. These Proposal objects are complex and I want to return a summarized JSON view of them. A similar situation happens with the Customer object. I'm trying to implement this with @JsonView annotations.

I wanted to ask if extending the views of the member object classes in the container object class view is the way to do this or, if not, if there is a cleaner way to implement this that I am unaware of.

Context

Before today, I was under the false impression that you could annotate your controller with multiple views and that the resulting JSON representation would be filtered accordingly.

@JsonView({Project.Extended.class, Proposal.Summary.class, Customer.Summary.class})
@Transactional
@RequestMapping(value="/project", method=RequestMethod.GET)
public @ResponseBody List<Project> findAll() {
    return projectDAO.findAll();    
}

Where each class had its own JsonView annotations and interfaces e.g.:

public class Customer {
    ...
    public interface Summary {}
    public interface Normal extends Summary {}
    public interface Extended extends Normal {}
}

Nevertheless, it is only the first view in the array that gets taken into account. According to https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring

Only one class or interface can be specified with the @JsonView annotation, but you can use inheritance to represent JSON View hierarchies (if a field is part of a JSON View, it will be also part of parent view). For example, this handler method will serialize fields annotated with @JsonView(View.Summary.class) and @JsonView(View.SummaryWithRecipients.class):

and the official documentation in http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-jsonview

To use it with an @ResponseBody controller method or controller methods that return ResponseEntity, simply add the @JsonView annotation with a class argument specifying the view class or interface to be used:

So, I ended up extending the views of the members in the view of the container object, like this

@Entity
public class Project {
    ...
    public static interface Extended extends Normal, Proposal.Extended {}
    public static interface Normal extends Summary, Customer.Normal {}
    public static interface Summary {}
}

and changed my controller to this

@JsonView(Project.Extended.class)
@Transactional
@RequestMapping(value="/project", method=RequestMethod.GET)
public @ResponseBody List<Project> findAll() {
    return projectDAO.findAll();    
}

This seems to do the trick, but I couldn't find documentation or discussion about this situation. Is this the intended use of JsonViews or is it kind of hackish?

Thank you in advance

-Patricio Marrone

like image 855
Patricio Marrone Avatar asked May 26 '15 19:05

Patricio Marrone


2 Answers

I believe you have configured your views as necessary. The root of the issue is not Spring's @JsonView, but rather Jackson's implementation of views. As stated in Jackson's view documentation:

Only single active view per serialization; but due to inheritance of Views, can combine Views via aggregation.

So, it appears that Spring is simply passing on and adhering to the limitation set in place by Jackson 2.

like image 54
DavidA Avatar answered Nov 03 '22 04:11

DavidA


I use Jersey+Jackson but issued just the same problem.

That's a trick that I'm doing for my application to let me require for several JSON Views during serialization. I bet it is also possible with Spring MVC instead of Jersey, but not 100% sure. It also does not seem to have performance issues. Maybe it is a bit complicated for your case, but if you have large object with big amount of possible views, maybe it's better than doing a lot of inheritance.

So I use the Jackson Filter approach to require several views in serialization. However, I haven't found the way to overcome the issue of putting @JsonFilter("name") above the classes to map, which does not make it so clean. But I mask it in custom annotation at least:

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonFilter(JSONUtils.JACKSON_MULTIPLE_VIEWS_FILTER_NAME)
public @interface JsonMultipleViews {}

The filter itself looks like this:

public class JsonMultipleViewsFilter extends SimpleBeanPropertyFilter {

    private Collection<Class<?>> wantedViews;

    public JsonMultipleViewsFilter(Collection<Class<?>> wantedViews) {
        this.wantedViews = wantedViews;
    }

    @Override
    public void serializeAsField( Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer ) throws Exception {
        if( include( writer ) ) {
            JsonView jsonViewAnnotation = writer.getAnnotation(JsonView.class);
            // serialize the field only if there is no @JsonView annotation or, if there is one, check that at least one
            // of view classes above the field fits one of required classes. if yes, serialize the field, if no - skip the field
            if( jsonViewAnnotation == null || containsJsonViews(jsonViewAnnotation.value()) ) {
                writer.serializeAsField( pojo, jgen, provider );
            }
        }
        else if( !jgen.canOmitFields() ) { 
            // since 2.3
            writer.serializeAsOmittedField( pojo, jgen, provider );
        }
    }    

    private boolean containsJsonViews(Class<?>[] viewsOfProperty) {
        for (Class<?> viewOfProperty : viewsOfProperty) {
            for (Class<?> wantedView : wantedViews) {
                // check also subclasses of required view class
                if (viewOfProperty.isAssignableFrom(wantedView)) {
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    protected boolean include( BeanPropertyWriter writer ) {
        return true;
    }

    @Override
    protected boolean include( PropertyWriter writer ) {
        return true;
    }
}

I can use this filter like this:

public static String toJson( Object object, Collection<Class<?>> jsonViewClasses) throws JsonProcessingException {
    // if no json view class is provided, just map without view approach
    if (jsonViewClasses.isEmpty()) {
        return mapper.writeValueAsString(object);
    }
    // if only one json view class is provided, use out of the box jackson mechanism for handling json views
    if (jsonViewClasses.size() == 1) {
        return mapper.writerWithView(jsonViewClasses.iterator().next()).writeValueAsString(object);
    }

    // if more than one json view class is provided, uses custom filter to serialize with multiple views
    JsonMultipleViewsFilter jsonMultipleViewsFilter = new JsonMultipleViewsFilter(jsonViewClasses);
    return mapper.writer(new SimpleFilterProvider() // use filter approach when serializing
                .setDefaultFilter(jsonMultipleViewsFilter) // set it as default filter in case of error in writing filter name
                .addFilter(JACKSON_MULTIPLE_VIEWS_FILTER_NAME, jsonMultipleViewsFilter) // set custom filter for multiple views with name
                .setFailOnUnknownId(false)) // if filter is unknown, don't fail, use default one
                .writeValueAsString(object);
}

After that, Jersey allows us to add Jersey Filters on the point of running the application (it goes through each endpoint in each Controller in start of application and we can easily bind the Jersey filters at this moment if there is is multiple value in @JsonView annotation above the endpoint).

In Jersey filter for @JsonView annotation with multiple value above endpoint, once it's bint on startup to correct endpoints depending on annotations, we can easily override the response entity with calling that utils method

toJson(previousResponeObjectReturned, viewClassesFromAnnoation);

No reason to provide the code of Jersey Filter here since you're using Spring MVC. I just hope that it's easy to do it the same way in Spring MVC.

The Domain Object would look like this:

@JsonMultipleViews
public class Example
{
    private int id;
    private String name;

    @JsonView(JsonViews.Extended.class)
    private String extendedInfo;

    @JsonView(JsonViews.Meta.class)
    private Date updateDate;

    public static class JsonViews {
        public interface Min {} 
        public interface Extended extends Min {} 
        public interface Meta extends Min {} 
        //...
        public interface All extends Extended, Meta {} // interfaces are needed for multiple inheritence of views
    }
}

We can ommit putting Min.class in my case on those fields that are always required not depending on view. We just put Min in required views and it will serialize all fields without @JsonView annotation.

View All.class is required for me since if we have, for example, a specific set of views for each domain class (like in my case) and then we need to map a complex model consisting of several domain objects that both use views approach - some view for object one, but all views for object two, it's easier to put it above endpoint like this:

 @JsonView({ObjectOneViews.SomeView.class, ObjectTwoViews.All.class})

because if we ommit ObjectTwoViews.All.class here and require for only ObjectOneViews.SomeView.class, those fields that are marked with annotation in Object Two will not be serialized.

like image 2
Din Avatar answered Nov 03 '22 04:11

Din