Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to return a partial JSON response using Java?

I'm building a RESTful API and want to provide developers with the option to choose which fields to return in the JSON response. This blog post shows examples of how several API's (Google, Facebook, LinkedIn) allow developers to customize the response. This is referred to as partial response.

An example might look like this:

/users/123?fields=userId,fullname,title

In the example above the API should return the userId, fullName and title fields for User "123".

I'm looking for ideas of how to implement this in my RESTful web service. I'm currently using CXF (edit: and Jackson) but willing to try another JAX-RS implementation.

Here's what I currently have. It returns a full User object. How can I return only the fields the API caller wants at runtime based on the "fields" paramaeter? I don't want to make the other fields Null. I simply don't want to return them.

@GET
@Path("/{userId}")
@Produces("application/json")
public User getUser(@PathParam("userId") Long userId, 
    @DefaultValue("userId,fullname,title") @QueryParam("fields") String fields) {

User user = userService.findOne(userId);

StringTokenizer st = new StringTokenizer(fields, ",");
while (st.hasMoreTokens()) {

    // here's where i would like to select only the fields i want to return

}
return user;
}

UPDATE:

I followed unludo's link which then linked to this: http://wiki.fasterxml.com/JacksonFeatureJsonFilter

With that info I added @JsonFilter("myFilter") to my domain class. Then I modified my RESTful service method to return String instead of User as follows:

@GET
@Path("/{userId}")
@Produces("application/json")
public String getUser(@PathParam("userId") Long userId,
                    @DefaultValue("userId,fullname,title") @QueryParam("fields") String fields) {

    User user = userService.findOne(userId);

    StringTokenizer st = new StringTokenizer(fields, ",");
    Set<String> filterProperties = new HashSet<String>();
    while (st.hasMoreTokens()) {
        filterProperties.add(st.nextToken());
    }

    ObjectMapper mapper = new ObjectMapper();
    FilterProvider filters = new SimpleFilterProvider().addFilter("myFilter",
                SimpleBeanPropertyFilter.filterOutAllExcept(filterProperties));

    try {
        String json = mapper.filteredWriter(filters).writeValueAsString(user);
        return json;
    } catch (IOException e) {
        e.printStackTrace();
    return e.getMessage();
    }
}

I need to do more testing but so far so good.

like image 234
Justin Avatar asked Feb 16 '12 16:02

Justin


3 Answers

If you use Jackson (a great JSON lib - kind of the standard for Java I believe), you may use the @View annotation to filter what you want in the resulting object.

I understand that you want something dynamic so it's a bit more complicated. You will find what you are looking for here: http://www.cowtowncoder.com/blog/archives/2011/02/entry_443.html (look at 6. Fully dynamic filtering: @JsonFilter).

I would be interested in the solution you will find.

like image 133
unludo Avatar answered Nov 07 '22 12:11

unludo


Creating an ObjectMapper instance inside the resource method for every request can have significant performance overhead. According to the Jackson performance best practices object mappers are expensive to create.

Instead you can customize the JAX-RS provider's Jackson object writer inside the resource method using the Jackson 2.3 ObjectWriterModifier/ObjectReaderModifier feature.

Here is an example shows how to register an ObjectWriterModifier thread local object that changes the set of the filters applied for the JAX-RS Jackson provider being used inside a resource method. Note that I have not tested the code against an JAX-RS implementation.

public class JacksonObjectWriterModifier2 {

    private static class FilterModifier extends ObjectWriterModifier {
        private final FilterProvider provider;

        private FilterModifier(FilterProvider provider) {
            this.provider = provider;
        }

        @Override
        public ObjectWriter modify(EndpointConfigBase<?> endpoint, MultivaluedMap<String, Object> responseHeaders,
                                   Object valueToWrite, ObjectWriter w, JsonGenerator g) throws IOException {
            return w.with(provider);
        }
    }

    @JsonFilter("filter1")
    public static class Bean {
        public final String field1;
        public final String field2;

        public Bean(String field1, String field2) {
            this.field1 = field1;
            this.field2 = field2;
        }
    }

    public static void main(String[] args) throws IOException {
        Bean b = new Bean("a", "b");
        JacksonJsonProvider provider = new JacksonJsonProvider();
        ObjectWriterInjector.set(new FilterModifier(new SimpleFilterProvider().addFilter("filter1",
                SimpleBeanPropertyFilter.filterOutAllExcept("field1"))));

        provider.writeTo(b, Bean.class, null, null, MediaType.APPLICATION_JSON_TYPE, null, System.out);
    }

}

Output:

{"field1":"a"}
like image 32
Alexey Gavrilov Avatar answered Nov 07 '22 13:11

Alexey Gavrilov


The Library jersey-entity-filtering Can do that :

https://github.com/jersey/jersey/tree/2.22.2/examples/entity-filtering-selectable

https://jersey.java.net/documentation/latest/entity-filtering.html

Exemple :

My Object

public class Address {

    private String streetAddress;
    private String region;
    private PhoneNumber phoneNumber;
}

URL

people/1234?select=streetAddress,region

RETURN

{
   "streetAddress": "2 square Tyson",
   "region": "Texas"
}

Add to Maven

<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-entity-filtering</artifactId>
    <version>2.22.2</version>
</dependency>
like image 4
Hydral Avatar answered Nov 07 '22 11:11

Hydral