Integrating Gson with JAX-RS (Jersey) for Seamless JSON Handling

JSON has become the de-facto standard for data exchange in web services, and for Java developers, Gson is a highly popular library for converting Java objects to JSON and vice-versa. When building RESTful APIs with JAX-RS (specifically Jersey), integrating Gson can significantly streamline your development process. This post will guide you through setting up a Jersey project to leverage Gson for automatic JSON serialization and deserialization.

Why Gson with JAX-RS?

While JAX-RS implementations like Jersey often come with their own default JSON providers (like Jackson), Gson offers a lightweight and often more intuitive API for many developers. Its simple approach to serialization and deserialization, along with features like custom type adapters and versioning, makes it a compelling choice for many projects.

Project Setup: Maven Dependencies

To begin, you’ll need to include the necessary dependencies in your pom.xml file. We’ll need the Jersey server dependency and the Gson library itself.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ankurm.blog</groupId>
    <artifactId>jersey-gson-example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <jersey.version>2.34</jersey.version> <!-- Or your desired Jersey version -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Jersey JAX-RS Server -->
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet-core</artifactId>
            <version>${jersey.version}</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
            <version>${jersey.version}</version>
        </dependency>

        <!-- Gson Library -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version> <!-- Or the latest stable version -->
        </dependency>

        <!-- For running embedded server (e.g., Jetty or Tomcat embedded) -->
        <!-- Depending on your setup, you might need a servlet API dependency in development -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version> <!-- Or appropriate version >= 3.1 -->
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Creating a Custom Gson MessageBodyReader/Writer

Jersey, by default, doesn’t know how to use Gson for JSON handling. We need to create custom JAX-RS MessageBodyReader and MessageBodyWriter implementations that leverage Gson. These will inform Jersey how to read and write Java objects as JSON using Gson.

1. The Employee Model

Let’s define a simple Employee class that we’ll be serializing/deserializing.

package com.ankurm.blog.model;

public class Employee {
    private int id;
    private String firstName;
    private String lastName;
    private String email;

    public Employee() {
    }

    public Employee(int id, String firstName, String lastName, String email) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }

    // Getters and Setters
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "Employee{" +
               "id=" + id +
               ", firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               ", email='" + email + '\'' +
               '}';
    }
}

2. GsonProvider: The Core Integration

This class will implement both MessageBodyReader and MessageBodyWriter. It will use a Gson instance to perform the actual JSON conversions.

package com.ankurm.blog.providers;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

@Provider // Register this as a JAX-RS provider
@Produces(MediaType.APPLICATION_JSON) // This provider produces JSON
@Consumes(MediaType.APPLICATION_JSON) // This provider consumes JSON
public class GsonProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object> {

    private final Gson gson;

    public GsonProvider() {
        // You can customize your Gson instance here, e.g., for pretty printing, date formats
        gson = new GsonBuilder()
                .setPrettyPrinting() // For pretty-printed JSON output
                // .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") // Example date format
                .create();
    }

    @Override
    public boolean isReadable(Class<?> type, Type genericType,
                              Annotation[] annotations, MediaType mediaType) {
        // This provider can read any class if the media type is JSON
        return mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE);
    }

    @Override
    public Object readFrom(Class<Object> type, Type genericType,
                           Annotation[] annotations, MediaType mediaType,
                           MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
            throws IOException, WebApplicationException {
        try (InputStreamReader reader = new InputStreamReader(entityStream, "UTF-8")) {
            return gson.fromJson(reader, genericType);
        }
    }

    @Override
    public boolean isWriteable(Class<?> type, Type genericType,
                               Annotation[] annotations, MediaType mediaType) {
        // This provider can write any class if the media type is JSON
        return mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE);
    }

    @Override
    public void writeTo(Object object, Class<?> type, Type genericType,
                        Annotation[] annotations, MediaType mediaType,
                        MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {
        try (OutputStreamWriter writer = new OutputStreamWriter(entityStream, "UTF-8")) {
            gson.toJson(object, genericType, writer);
        }
    }

    @Override
    public long getSize(Object object, Class<?> type, Type genericType,
                        Annotation[] annotations, MediaType mediaType) {
        // Deprecated by JAX-RS 2.0. Return -1 indicates that
        // the message body writer is not able to determine the length in advance.
        return -1;
    }
}

Explanation of GsonProvider:

  • @Provider: This annotation tells Jersey to discover and register this class as a JAX-RS provider.
  • @Produces(MediaType.APPLICATION_JSON) and @Consumes(MediaType.APPLICATION_JSON): These annotations specify that this provider handles JSON media types.
  • isReadable and isWriteable: These methods determine if the provider can handle a given type and media type. Our implementation makes it generic for any class and JSON.
  • readFrom: This method takes an InputStream (the request body), uses Gson to deserialize it into the target Java object, and returns the object.
  • writeTo: This method takes a Java object, uses Gson to serialize it into JSON, and writes it to the OutputStream (the response body).
  • getSize: This method is deprecated and can return -1.

The JAX-RS Resource

Now, let’s create a simple JAX-RS resource that exposes endpoints for our Employee model. Jersey will automatically use our GsonProvider for JSON conversions.

package com.ankurm.blog.resources;

import com.ankurm.blog.model.Employee;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;

@Path("/employees")
public class EmployeeResource {

    private static List<Employee> employees = new ArrayList<>();

    static {
        employees.add(new Employee(1, "Ankur", "M", "[email protected]"));
        employees.add(new Employee(2, "Jane", "Doe", "[email protected]"));
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response getAllEmployees() {
        return Response.ok(employees).build();
    }

    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getEmployeeById(@PathParam("id") int id) {
        Employee employee = employees.stream()
                .filter(e -> e.getId() == id)
                .findFirst()
                .orElse(null);
        if (employee != null) {
            return Response.ok(employee).build();
        } else {
            return Response.status(Response.Status.NOT_FOUND).entity("Employee not found").build();
        }
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response addEmployee(Employee employee) {
        if (employee.getId() == 0) { // Simple validation for new employee
            employee.setId(employees.size() + 1); // Assign a new ID
        }
        employees.add(employee);
        return Response.status(Response.Status.CREATED).entity(employee).build();
    }

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response updateEmployee(Employee updatedEmployee) {
        for (int i = 0; i < employees.size(); i++) {
            if (employees.get(i).getId() == updatedEmployee.getId()) {
                employees.set(i, updatedEmployee);
                return Response.ok(updatedEmployee).build();
            }
        }
        return Response.status(Response.Status.NOT_FOUND).entity("Employee to update not found").build();
    }

    @DELETE
    @Path("/{id}")
    public Response deleteEmployee(@PathParam("id") int id) {
        boolean removed = employees.removeIf(e -> e.getId() == id);
        if (removed) {
            return Response.noContent().build();
        } else {
            return Response.status(Response.Status.NOT_FOUND).entity("Employee not found").build();
        }
    }
}

JAX-RS Application Configuration

Finally, we need to tell Jersey about our application classes and providers. We do this by extending ResourceConfig (or Application). Jersey will automatically discover classes annotated with @Path and @Provider if they are in the configured package(s).

package com.ankurm.blog.config;

import com.ankurm.blog.providers.GsonProvider;
import com.ankurm.blog.resources.EmployeeResource;
import org.glassfish.jersey.server.ResourceConfig;

import javax.ws.rs.ApplicationPath;

@ApplicationPath("/api") // Base path for all REST endpoints
public class MyApplication extends ResourceConfig {
    public MyApplication() {
        // Register JAX-RS resources and providers
        // Option 1: Register classes directly
        // register(EmployeeResource.class);
        // register(GsonProvider.class);

        // Option 2: Scan packages for resources and providers (recommended for larger apps)
        packages("com.ankurm.blog.resources", "com.ankurm.blog.providers");
    }
}

Deployment (web.xml)

For a traditional WAR deployment, you’ll configure Jersey in your web.xml to use your MyApplication class.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <display-name>Jersey Gson Example</display-name>

    <servlet>
        <servlet-name>Jersey Web Application</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>com.ankurm.blog.config.MyApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>Jersey Web Application</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping>

</web-app>

Testing the API

Deploy your WAR file to a servlet container (like Tomcat or Jetty). Assuming your context root is /jersey-gson-example and your ApplicationPath is /api, your base URL will be http://localhost:8080/jersey-gson-example/api.

You can use tools like Postman, Insomnia, or curl to test the endpoints:

1. Get All Employees

curl -X GET http://localhost:8080/jersey-gson-example/api/employees \
     -H "Accept: application/json"

Expected Output:

[
      {
        "id": 1,
        "firstName": "Ankur",
        "lastName": "M",
        "email": "[email protected]"
      },
      {
        "id": 2,
        "firstName": "Jane",
        "lastName": "Doe",
        "email": "[email protected]"
      }
    ]

2. Get Employee by ID

curl -X GET http://localhost:8080/jersey-gson-example/api/employees/1 \
     -H "Accept: application/json"

Expected Output:

{
      "id": 1,
      "firstName": "Ankur",
      "lastName": "M",
      "email": "[email protected]"
    }

3. Add a New Employee

curl -X POST http://localhost:8080/jersey-gson-example/api/employees \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{
           "firstName": "John",
           "lastName": "Smith",
           "email": "[email protected]"
         }'

Expected Output (with new ID):

{
      "id": 3,
      "firstName": "John",
      "lastName": "Smith",
      "email": "[email protected]"
    }

4. Update an Employee

curl -X PUT http://localhost:8080/jersey-gson-example/api/employees \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{
           "id": 1,
           "firstName": "Ankur",
           "lastName": "M. Updated",
           "email": "[email protected]"
         }'

Expected Output:

{
      "id": 1,
      "firstName": "Ankur",
      "lastName": "M. Updated",
      "email": "[email protected]"
    }

5. Delete an Employee

curl -X DELETE http://localhost:8080/jersey-gson-example/api/employees/2

Expected Output:

HTTP 204 No Content

Conclusion

By implementing a custom MessageBodyReader and MessageBodyWriter, you can seamlessly integrate Gson with your JAX-RS (Jersey) applications. This approach provides fine-grained control over Gson’s configuration and ensures that your RESTful services efficiently handle JSON data. Happy coding!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.