JAX-RS - Using @QueryParam for Optional Parameters

[Updated: Apr 25, 2017, Created: Dec 24, 2015]

In the last tutorial we saw how to construct resource path and corresponding usage of JAX-RS annotations. Sometimes we may want to give some options to the client to refine their queries. For example clients may want to filter the output by specifying some optional parameter. The resource like http://example.com/api/orders might return thousands of orders. The client might be interested, for example, only in orders placed by the customers living in a particular city or state. Should we include that info to our resource URI as path? i.e. http://example.com/api/orders/stateCode. I guess that's not a good idea as filtering orders by state code is only optional not mandatory. Putting optional parameters as the URI path will end up in a lot of path variations which will be very difficult to maintain. Any optional parameters should certainly be query string parameters. So in our example the resource should be http://example.com/api/orders?stateCode=TX. Followings are the details how we are going to do handle such request on server side using JAX-RS API:

  1. @QueryParam is used to handle query parameters.
    @Path("/orders")
    public class OrderService {
    
        @GET
        public String getOrders(@QueryParam("stateCode") String stateCode) {
            return stateCode != null ? getOrdersByState(stateCode)
                                      : getAllOrders();
        }
    }
    The query parameter will be null if user just request http://example.com/api/orders.
  2. Consider the case if we define more query parameters on the same resource. For example we want to do pagination as well i.e. http://example.com/api/orders?startPage=10&pagesSize=20. You might be thinking we should do something like this:
    @Path("/orders")
    public class OrderService {
        @GET
        public String getOrders(@QueryParam("stateCode") String stateCode) {
            return stateCode != null ? getOrdersByState(stateCode)
                                      : getAllOrders();
        }
    
        @GET
        public String getOrders(@QueryParam("pageStart") int pageStart,
                                @QueryParam("pagesSize") int pageSize) {
            return pageStart != 0 && pageSize != 0
                                     ? getOrdersByIndex(pageStart, pageSize)
                                     : getAllOrders();
        }
    }
    The above code will result in a runtime error (startup time error actually, Jersey does all necessary validation during startup time). A RESTful resource is uniquely defined by its path, and not by its params. Two parameter groups we defined have the same path. Either the two methods should have different HTTP verbs or different URI paths (there's one more feature 'media type' which can uniquely define the resource). So how can we handle the above case? Well, we have no choice but to combine two groups of parameters into a single method. The resultant method will have three query parameters. If user is not going to specify some parameter, it's value will be passed as null (that's how we are going to make decisions, based on null values or default values).
        @GET
        public String getOrders(@QueryParam("stateCode") String stateCode,
                                @QueryParam("pageStart") int pageStart,
                                @QueryParam("pagesSize") int pageSize) {
            ....
        }
            
    Note that we defined pageStart and pagesSize as int. JAX-RS does the similar conversions as in case of @ParamPath (as listed next). If user doesn't specify any values for primitive int values it will have 0 value (The rules of Java default values for instance variables are applied).
  3. There's an automatic conversion for the method parameters annotated with @PathParam, if we declared java parameter type as:
    • Primitive type
    • Number wrappers (generally speaking any class, having static method ValueOf(String))
    • Any class that accepts a single String argument in its constructor.
    • List<T>, Set<T> or SortedSet<T>, where T satisfies 2 or 3 above. The collection instance passed, is read-only.
  4. @DefaultValue annotation can be used along with @QueryParam to specify a default value.
    @GET
    public String getOrders(@DefaultValue("1")
                            @QueryParam("pageStart")
                                       int pageStart,
                            @DefaultValue("10")
                            @QueryParam("pagesSize")
                                       int pageSize) {
             ....
    }
            
  5. Using UriInfo: Instead of using @QueryParam .We can inject UriInfo with @Context annotation. This interface has many convenient methods to get URI information. In our case we can use the method UriInfo#getPathParameters() to access query parameters. Following snippet shows our modified getOrders method.
    @Path("/orders")
    public class OrderService {
    
        @GET
        public String getOrders(@Context UriInfo uriInfo) {
            MultivaluedMap<String, String> params =
                    uriInfo.getQueryParameters();
    
            String message = "Starting with all orders. ";
            String stateCode = params.getFirst("stateCode");
            if (stateCode != null) {
                //validate state code first and then filter orders
                message += "Orders filtered by the state: " + stateCode + ". ";
            }
            String pageStart = params.getFirst("pageStart");
            String pagesSize = params.getFirst("pagesSize");
            if (pageStart != null && pagesSize != null) {
                //validate and parse pagination params into int then
                //filter orders
                message += "Performed pagination on the orders. pageStart: "
                        + pageStart + ", pagesSize: " + pagesSize;
            }
            return message;
        }
    }

Example Project

Here's the example project, covering almost all points explained above together with additional methods, specially handling query param on nested level i.e. http://example.com/api/orders/453/items?minPrice=100. The steps to create the project are same as our last examples.

Here is our REST API which we extended from last tutorial along with additional query parameter.

  1. http://example.com/api/orders
    Query params:
    • Filter orders by state:
      http://example.com/api/orders?stateCode={stateCode}
    • Pagination:
      http://example.com/api/orders?pageStart={pageStart}&pagesSize={pagesSize}
    • Filter by date range:
      http://example.com/api/orders?dateFrom={dateFrom}&dateTo={dateTo}
  2. http://example.com/api/orders/{orderId}
  3. http://example.com/api/orders/{orderId}/items
    Query params:
    • Following URI handles requests for the items of a particular order, having price greater than 'minPrice':
      http://example.com/api/orders/{orderId}/items?minPrice={minPrice}

Dependencies and Technologies Used :

  • jersey-core-server 2.22.1: Jersey core server implementation.
  • jersey-container-servlet 2.22.1: Jersey core Servlet 3.x implementation.
  • JDK 1.8
  • Maven 3.0.4

Rest Query Param Example Select All Download
  • rest-query-param
    • src
      • main
        • java
          • com
            • logicbig
              • rest
                • api

See Also