August 14, 2022

JSON example with Jersey + Jackson

This article shows how to return a JSON response in the Jersey application, using Jackson 2.x.

Tested with

  • Jersey 3.0.2
  • Grizzly 3 HTTP Server
  • Jackson 2.12.2
  • Java 11
  • Maven
  • JUnit 5 and JSONassert 1.5 (Unit Test)

1. Jackson as the JSON provider in Jersey

We can include jersey-media-json-jackson to enable Jackson as the JSON provider in the Jersey application.

pom.xml

  <!-- add jackson as json provider -->
  <dependency>
      <groupId>org.glassfish.jersey.media</groupId>
      <artifactId>jersey-media-json-jackson</artifactId>
  </dependency>

2. Project Directory

Review the Maven standard project directory.

3. Project dependencies

Review the project dependencies.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.favtuts</groupId>
    <artifactId>jersey-json-jackson-example</artifactId>
    <packaging>jar</packaging>
    <version>1.0</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
        <junit.version>5.4.0</junit.version>
        <jsonassert.version>1.5.0</jsonassert.version>
        <jersey.version>3.0.2</jersey.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>

        <!-- Grizzly 2 HTTP Server -->
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-grizzly2-http</artifactId>
        </dependency>

        <!-- Jersey DI and core-->
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
        </dependency>

        <!-- add jackson as json provider -->
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
        </dependency>

        <!-- Need this to hide warning for jakarta.activation.DataSource -->
        <dependency>
            <groupId>jakarta.activation</groupId>
            <artifactId>jakarta.activation-api</artifactId>
            <version>2.0.1</version>
        </dependency>

        <!-- JUnit 5 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- test json data -->
        <dependency>
            <groupId>org.skyscreamer</groupId>
            <artifactId>jsonassert</artifactId>
            <version>${jsonassert.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <finalName>jersey-json-jackson-example</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>

            <!-- JUnit 5 need at least 2.22.0 to support -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M5</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>com.favtuts.MainApp</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <!-- copy project dependencies -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.1.2</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <!-- exclude junit, we need runtime dependency only -->
                            <includeScope>runtime</includeScope>
                            <outputDirectory>${project.build.directory}/lib/</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

</project>

4. Jersey endpoints and return a JSON response

Create a few endpoints in Jersey, and Jackson will handle the object from/to JSON conversion.

4.1 Create the following endpoints and return JSON response.

  • GET /json/, returns a JSON string.
  • GET /json/{name}, returns an User object containg the {name} in JSON string.
  • GET /json/all, returns a list of User objects in JSON string.
  • POST /json/create, accepts JSON data and returns a status 201.

JsonResource.java

package com.favtuts.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import java.util.Arrays;
import java.util.List;

@Path("/json")
public class JsonResource {

    private static final ObjectMapper mapper = new ObjectMapper();

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response hello() {

        // create a JSON string
        ObjectNode json = mapper.createObjectNode();
        json.put("result", "Jersey JSON example using Jackson 2.x");
        return Response.status(Response.Status.OK).entity(json).build();
    }

    // Object to JSON
    @Path("/{name}")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public User hello(@PathParam("name") String name) {
        return new User(1, name);
    }

    // A list of objects to JSON
    @Path("/all")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<User> helloList() {
        return Arrays.asList(
                new User(1, "favtuts"),
                new User(2, "zilap")
        );
    }

    @Path("/create")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response create(User user) {

        ObjectNode json = mapper.createObjectNode();
        json.put("status", "ok");
        return Response.status(Response.Status.CREATED).entity(json).build();

    }

}

4.2 Below is the User object; later, Jackson will convert this object to/from a JSON string.

User.java

package com.favtuts.json;

public class User {

  private int id;
  String name;

  public User() {
  }

  public User(int id, String name) {
      this.id = id;
      this.name = name;
  }

  //  getters and setters
}

5. Start Jersey application

Starts the Jersey application at http://localhost:8080.

MainApp.java

package com.favtuts;

import com.favtuts.json.JsonResource;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;

import java.io.IOException;
import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;

public class MainApp {

    public static final URI BASE_URI = URI.create("http://localhost:8080");

    // Starts Grizzly HTTP server
    public static HttpServer startHttpServer() {

        final ResourceConfig config = new ResourceConfig();
        config.register(JsonResource.class);
        // JacksonFeature for JAXB/POJO, for pure JSON, no need this JacksonFeature
        // config.register(JacksonFeature.class);

        return GrizzlyHttpServerFactory.createHttpServer(BASE_URI, config);
    }

    public static void main(String[] args) {

        try {

            final HttpServer server = startHttpServer();

            server.start();

            // shut down hook
            Runtime.getRuntime().addShutdownHook(new Thread(server::shutdownNow));

            System.out.println(
              String.format("Application started.%nStop the application using CTRL+C"));

            // block and wait shut down signal, like CTRL+C
            Thread.currentThread().join();

        } catch (InterruptedException | IOException ex) {
            Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

}

6. DEMO

Starts the MainApp and using the cURL tool to do simple testing.

Terminal

$ curl http://localhost:8080/json
{"result":"Jersey JSON example using Jackson 2.x"}

$ curl http://localhost:8080/json/favtuts
{"id":1,"name":"favtuts"}

$ curl http://localhost:8080/json/all
[{"id":1,"name":"favtuts"},{"id":2,"name":"zilap"}]

# send a POST request with JSON data
$ curl -H "Content-Type: application/json"
    -X POST -d "{\"id\" : 1,\"name\" : \"favtuts\"}" http://localhost:8080/json/create

{"status":"ok"}

7. Custom Jackson object mapper in Jersey

In the above demo, the JSON response is in compact mode, the default behavior of Jackson ObjectMapper; and we can create a custom Jackson ObjectMapper in Jersey application by implementing the JAX RS ContextResolver class.

7.1 Enable the Jackson pretty print mode

Below example, create a custom Jackson ObjectMapper to enable the pretty print mode.

CustomJacksonMapperProvider.java

package com.favtuts.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;

// enable Jackson pretty print
@Provider
public class CustomJacksonMapperProvider
  implements ContextResolver<ObjectMapper> {

  final ObjectMapper mapper;

  public CustomJacksonMapperProvider() {
      // enable pretty print
      mapper = new ObjectMapper()
              .enable(SerializationFeature.INDENT_OUTPUT);
  }

  @Override
  public ObjectMapper getContext(Class<?> type) {
      return mapper;
  }

}

7.2 Register custom Jackson object mapper in ResourceConfig

MainApp.java

  public static HttpServer startHttpServer() {

      final ResourceConfig config = new ResourceConfig();
      config.register(JsonResource.class);

      // register the custom Jackson mapper
      config.register(CustomJacksonMapperProvider.class);

      return GrizzlyHttpServerFactory.createHttpServer(BASE_URI, config);
  }

7.3 Demo with pretty print enabled

Starts the MainApp again, and now the JSON response is in a pretty print string.

Terminal

$ curl http://localhost:8080/json/java
{
  "id" : 1,
  "name" : "java"
}

$ curl http://localhost:8080/json/all
[ {
  "id" : 1,
  "name" : "favtuts"
}, {
  "id" : 2,
  "name" : "zilap"
} ]

8. Custom JsonMappingException in Jersey

We try to POST an unrecognized field to /json/create, and Jersey will return a detailed default message.

Terminal

$ curl -H "Content-Type: application/json"
    -X POST -d "{\"id-error-field\" : 1,\"name\" : \"favtuts\"}" http://localhost:8080/json/create

Unrecognized field "id-error-field" (class com.favtuts.json.User), not marked as ignorable (2 known properties: "id", "name"])
 at [Source: (org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream); line: 1, column: 22]
 (through reference chain: com.favtuts.json.User["id-error-field"])

8.1 Custom JsonMappingException

We can create a custom ExceptionMapper to intercept the JsonMappingException error and return our custom JSON response.

CustomJsonExceptionMapper.java

package com.favtuts.json;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
public class CustomJsonExceptionMapper
      implements ExceptionMapper<JsonMappingException> {

  private static final ObjectMapper mapper = new ObjectMapper();

  @Override
  public Response toResponse(JsonMappingException exception) {

      ObjectNode json = mapper.createObjectNode();
      //json.put("error", exception.getMessage());
      json.put("error", "json mapping error");
      return Response.status(Response.Status.BAD_REQUEST)
              .entity(json.toPrettyString())
              .build();
  }

}

8.2 Register custom JsonMappingException in ResourceConfig

MainApp.java

    public static HttpServer startHttpServer() {

        final ResourceConfig config = new ResourceConfig();
        config.register(JsonResource.class);
        config.register(CustomJacksonMapperProvider.class);

        // custom ExceptionMapper
        config.register(CustomJsonExceptionMapper.class);

        return GrizzlyHttpServerFactory.createHttpServer(BASE_URI, config);
    }

8.3 DEMO with custom JsonMappingException

Starts the MainApp again, and we POST an unrecognized field to /json/create again, and now Jersey will return our custom JSON response.

Terminal

$ curl -H "Content-Type: application/json"
    -X POST -d "{\"id-error-field\" : 1,\"name\" : \"favtuts\"}" http://localhost:8080/json/create

{
  "error" : "json mapping error"
}  

9. Unit test a Jersey JSON endpoints

Below is a JUnit 5 and JSONAssert example to test the Jersey JSON endpoints.

JsonResourceTest.java

package com.favtuts;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.glassfish.grizzly.http.server.HttpServer;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class JsonResourceTest {

  private static HttpServer server;
  private static WebTarget target;

  @BeforeAll
  public static void beforeAllTests() {
      server = MainApp.startHttpServer();
      Client c = ClientBuilder.newClient();
      target = c.target(MainApp.BASE_URI.toString());
  }

  @AfterAll
  public static void afterAllTests() {
      server.shutdownNow();
  }

  @Test
  public void testJson() throws JSONException {

      String actual = target.path("json").request().get(String.class);
      String expected = "{\"result\":\"Jersey JSON example using Jackson 2.x\"}";

      JSONAssert.assertEquals(expected, actual, false);

  }

  @Test
  public void testJsonName() throws JSONException {

      String response = target.path("json/favtuts")
              .request(MediaType.APPLICATION_JSON)
              .get(String.class);

      // convert json string to JSONObject
      JSONObject actual = new JSONObject(response);

      String expected = "{\"id\":1,\"name\":\"favtuts\"}";
      JSONAssert.assertEquals(expected, actual, false);

  }

  @Test
  public void testJsonAll() throws JSONException {

      String response = target.path("json/all")
              .request(MediaType.APPLICATION_JSON)
              .get(String.class);

      // convert json string to JSONArray
      JSONArray actual = new JSONArray(response);

      String expected = "[{\"id\":1,\"name\":\"favtuts\"},{\"id\":2,\"name\":\"zilap\"}]";
      JSONAssert.assertEquals(expected, actual, false);

  }

  @Test
  public void testJsonCreateOk() throws JSONException {

      String json = "{\"id\":1,\"name\":\"favtuts\"}";

      Response response = target.path("json/create")
              .request(MediaType.APPLICATION_JSON)
              .post(Entity.entity(json, MediaType.valueOf("application/json")));

      assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());

      // read response body
      String actual = response.readEntity(String.class);
      String expected = "{\"status\":\"ok\"}";
      JSONAssert.assertEquals(expected, actual, false);

  }

  @Test
  public void testJsonCreateError() throws JSONException {

      String json = "{\"id_no_field\":1,\"name\":\"favtuts\"}";

      Response response = target.path("json/create")
              .request(MediaType.APPLICATION_JSON)
              .post(Entity.entity(json, MediaType.valueOf("application/json")));

      assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());

      String actual = response.readEntity(String.class);
      String expected = "{\"error\":\"json mapping error\"}";
      JSONAssert.assertEquals(expected, actual, false);
  }

}

Download Source Code

$ git clone https://github.com/favtuts/java-jax-rs-tutorials.git

$ cd jersey-json-jackson-example

$ mvn clean package

$ java -jar target/jersey-json-jackson-example.jar

11. References

Leave a Reply

Your email address will not be published.