What is Content Negotiation?

One of the more popular features of REST applications is the ability to return different representations of resources. For instance, data can be in JSON format or in XML. Or it can even be available in either format depending on the request. Content negotiation is the way for clients and servers to agree on what content format is sent.

Data is returned in multiple formats because the needs of each client's request can be different. A browser might prefer JSON format. Another command line based client might prefer XML format. A third client might request the same resource as a PNG image.

It is up to the service to determine which formats are supported.

There are many practical ways of performing content negotiation.

Content Negotiation Based on URL

Many popular public REST APIs perform content negotiation based on the URL. For instance, a resource in XML format might be at http://example.com/resource.xml. The same resource could be offered in JSON format but located at http://example.com/resource.json.

@Path("/resources")
public class Resource {

    @Path("{resourceID}.xml")
    @GET
    public Response getResourceInXML(@PathParam("resourceID") String resourceID) {
        return Response.ok(/* entity in XML format */).type(MediaType.APPLICATION_XML).build();
    }

    @Path("{resourceID}.json")
    @GET
    public Response getResourceInJSON(@PathParam("resourceID") String resourceID) {
        return Response.ok(/* entity in JSON format */).type(MediaType.APPLICATION_JSON).build();
    }
}

In the above code, a request to "/resources/resourceID.myformat" would result in a 404 status code.

Another way of implementing the above is to use a single resource method like below:

@Path("/resources")
public class Resource {

    @Path("{resourceID}.{type}")
    @GET
    public Response getResource(@PathParam("resourceID") String resourceID, @PathParam("type") String type) {
        if ("xml".equals(type)) {
            return Response.ok(/* entity in XML format */).type(MediaType.APPLICATION_XML).build();
        } else if ("json".equals(type)) {
            return Response.ok("/* entity in JSON format */).type(MediaType.APPLICATION_JSON).build();
        }

        return Response.status(404).build();
    }

}

The previous code excerpt essentially behaves the same as the ContentNegotiationViaURL.java example.

Content Negotiation Based on Request Parameter

Another popular method is for the client to specify the format in a parameter. For instance, by default, a resource could be offered in XML at http://example.com/resource. The JSON version could be retrieved by using a query parameter like http://example.com/resource?format=json.

@Path("/resources")
public class Resource {

    @Path("{resourceID}")
    @GET
    public Response getResource(@PathParam("resourceID") String resourceID,
                                 @QueryParam("format") String format) {
        if (format == null || "xml".equals(format)) {
            return Response.ok(/* entity in XML format */).type(MediaType.APPLICATION_XML).build();
        } else if ("json".equals(format)) {
            return Response.ok(/* entity in JSON format */).type(MediaType.APPLICATION_JSON).build();
        }

        return Response.notAcceptable(Variant.mediaTypes(MediaType.APPLICATION_XML_TYPE,
                                                         MediaType.APPLICATION_JSON_TYPE).add()
            .build()).build();
    }
}

Content Negotiation Based on Accept Header

Perhaps the most powerful form of content negotiation, the HTTP Accept header is another way of specifying the preferred representation format.

The following excerpt shows sample client code using the Wink RestClient:

    RestClient client = new RestClient();
    ClientResponse response = client.resource("http://example.com/resources/resource1").header("Accept", "application/json;q=1.0, application/xml;q=0.8").get();
    // find what format was sent back
    String contentType = response.getHeaders().getFirst("Content-Type");

    if (contentType.contains("application/json")) {
        JSONObject entity = response.getEntity(JSONObject.class); /* or use a String, InputStream, or other provider that supports the entity media type */
    } else if (contentType.contains("application/xml") {
        String entity = response.getEntity(String.class); /* or use a JAXB class, InputStream, etc. */
    }

The following example implements sample client code using the Apache HttpClient.

    HttpClient client = new HttpClient();
    GetMethod getMethod =
        new GetMethod("http://example.com/resources/resource1");
    // prefer JSON over XML but both are acceptable to the client
    getMethod.setRequestHeader("Accept", "application/json;q=1.0, application/xml;q=0.8");
    try {
        client.executeMethod(getMethod);

        // find what format was sent back
        getMethod.getResponseHeader("Content-Type").getValue();

        // use getMethod.getResponseBodyAsString() or getMethod.getResponseBodyAsStream()
        // to read the body
    } finally {
        getMethod.releaseConnection();
    }

Using the @Context HttpHeaders injected variable, the client preferred response representation is found.

@Path("/resources")
public class Resource {

    @Context
    private HttpHeaders headers;

    @Path("{resourceID}")
    @GET
    public Response getResource(@PathParam("resourceID") String resourceID) {
        List<MediaType> acceptHeaders = headers.getAcceptableMediaTypes();
        if (acceptHeaders == null || acceptHeaders.size() == 0) {
            return Response.ok(/* entity in XML format */).type(MediaType.APPLICATION_XML).build();
        }

        for (MediaType mt : acceptHeaders) {
            String qValue = mt.getParameters().get("q");
            if(qValue != null && Double.valueOf(qValue).doubleValue() == 0.0) {
                break;
            }
            if(MediaType.APPLICATION_XML_TYPE.isCompatible(mt)) {
                return Response.ok(/* entity in XML format */).type(MediaType.APPLICATION_XML).build();
            } else if (MediaType.APPLICATION_JSON_TYPE.isCompatible(mt)) {
                return Response.ok(/* entity in JSON format */).type(MediaType.APPLICATION_JSON).build();
            }
        }
        return Response.notAcceptable(Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE,
                                                         MediaType.APPLICATION_XML_TYPE).add()
            .build()).build();
    }

}

If the client request does not have an Accept HTTP header, then by default the XML format is returned. The @Context HttpHeaders.getAcceptableMediaTypes() method returns an ordered list, sorted by the client preference of the representations.

Looping through the acceptable media types, if the preferred media type is compatible with one of the service's available return types, then the preferred media type is used.

Note that the quality factor is checked. In fairly rare requests, clients can let the service know that a media type should not be sent back (if the quality factor is 0.0).