Main Tutorials

Jersey and JSON examples (EclipseLink MOXy)

jersey json MOXy

This article shows how to return a JSON response in the Jersey application, using EclipseLink MOXy.

Tested with

  • Jersey 3.0.2
  • EclipseLink MOXy 3
  • Jetty 11, HTTP Server
  • Java 11
  • Maven 3
  • SLF4J, Logback, redirect Jersey J.U.L logs to logback
  • JUnit 5 and JSONassert 1.5 (Unit Test)
  • org.json 20210307, JSONObject

Table of contents

We can include jersey-media-moxy to make EclipseLink MOXy the JSON provider in the Jersey application.

pom.xml

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

2. Project directory

Review the Maven standard project directory.

jersey moxy project

3. Project dependencies

Review the project dependencies.

pom.xml

  <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>
      <logback.version>1.2.3</logback.version>
      <slf4j.version>1.7.31</slf4j.version>
      <org.json.version>20210307</org.json.version>
      <servlet.api.version>5.0.0</servlet.api.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>

      <!-- Jetty HTTP Server -->
      <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-jetty-http</artifactId>
      </dependency>

      <!-- Jetty 11 needs Servlet 5 -->
      <!-- java.lang.NoClassDefFoundError: jakarta/servlet/ServletInputStream -->
      <dependency>
          <groupId>jakarta.servlet</groupId>
          <artifactId>jakarta.servlet-api</artifactId>
          <version>${servlet.api.version}</version>
      </dependency>

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

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

      <!-- optional, only if you want the JSONObject -->
      <dependency>
          <groupId>org.json</groupId>
          <artifactId>json</artifactId>
          <version>${org.json.version}</version>
      </dependency>

      <!-- logging, bridge jul to slf4j -->
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jul-to-slf4j</artifactId>
          <version>${slf4j.version}</version>
      </dependency>

      <!-- slf4j implementation -->
      <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>${logback.version}</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>
Terminal

mvn dependency:tree

[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.mkyong:jersey-json-example >-------------------
[INFO] Building jersey-json-example 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:3.1.2:tree (default-cli) @ jersey-json-example ---
[INFO] com.mkyong:jersey-json-example:jar:1.0
[INFO] +- org.glassfish.jersey.containers:jersey-container-jetty-http:jar:3.0.2:compile
[INFO] |  +- jakarta.inject:jakarta.inject-api:jar:2.0.0:compile
[INFO] |  +- org.eclipse.jetty:jetty-server:jar:11.0.0:compile
[INFO] |  |  +- org.eclipse.jetty:jetty-http:jar:11.0.0:compile
[INFO] |  |  \- org.eclipse.jetty:jetty-io:jar:11.0.0:compile
[INFO] |  +- org.eclipse.jetty:jetty-util:jar:11.0.0:compile
[INFO] |  +- org.glassfish.jersey.core:jersey-common:jar:3.0.2:compile
[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api:jar:2.0.0:compile
[INFO] |  |  \- org.glassfish.hk2:osgi-resource-locator:jar:1.0.3:compile
[INFO] |  +- org.glassfish.jersey.core:jersey-server:jar:3.0.2:compile
[INFO] |  |  +- org.glassfish.jersey.core:jersey-client:jar:3.0.2:compile
[INFO] |  |  \- jakarta.validation:jakarta.validation-api:jar:3.0.0:compile
[INFO] |  \- jakarta.ws.rs:jakarta.ws.rs-api:jar:3.0.0:compile
[INFO] +- jakarta.servlet:jakarta.servlet-api:jar:5.0.0:compile
[INFO] +- org.glassfish.jersey.inject:jersey-hk2:jar:3.0.2:compile
[INFO] |  +- org.glassfish.hk2:hk2-locator:jar:3.0.1:compile
[INFO] |  |  +- org.glassfish.hk2.external:aopalliance-repackaged:jar:3.0.1:compile
[INFO] |  |  +- org.glassfish.hk2:hk2-api:jar:3.0.1:compile
[INFO] |  |  \- org.glassfish.hk2:hk2-utils:jar:3.0.1:compile
[INFO] |  \- org.javassist:javassist:jar:3.25.0-GA:compile
[INFO] +- org.glassfish.jersey.media:jersey-media-moxy:jar:3.0.2:compile
[INFO] |  +- org.glassfish.jersey.ext:jersey-entity-filtering:jar:3.0.2:compile
[INFO] |  +- jakarta.json:jakarta.json-api:jar:2.0.0:compile
[INFO] |  +- jakarta.json.bind:jakarta.json.bind-api:jar:2.0.0:compile
[INFO] |  +- org.glassfish:jakarta.json:jar:module:2.0.0:compile
[INFO] |  \- org.eclipse.persistence:org.eclipse.persistence.moxy:jar:3.0.0:compile
[INFO] |     +- jakarta.xml.bind:jakarta.xml.bind-api:jar:3.0.0:compile
[INFO] |     +- org.eclipse.persistence:org.eclipse.persistence.asm:jar:3.0.0:compile
[INFO] |     +- org.eclipse.persistence:org.eclipse.persistence.core:jar:3.0.0:compile
[INFO] |     +- com.sun.activation:jakarta.activation:jar:2.0.0:runtime
[INFO] |     \- com.sun.mail:jakarta.mail:jar:2.0.0:runtime
[INFO] +- org.json:json:jar:20210307:compile
[INFO] +- org.slf4j:jul-to-slf4j:jar:1.7.31:compile
[INFO] |  \- org.slf4j:slf4j-api:jar:1.7.31:compile
[INFO] +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] |  \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] +- org.junit.jupiter:junit-jupiter-params:jar:5.4.0:test
[INFO] |  +- org.apiguardian:apiguardian-api:jar:1.0.0:test
[INFO] |  \- org.junit.jupiter:junit-jupiter-api:jar:5.4.0:test
[INFO] |     +- org.opentest4j:opentest4j:jar:1.1.1:test
[INFO] |     \- org.junit.platform:junit-platform-commons:jar:1.4.0:test
[INFO] \- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO]    \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  7.664 s
[INFO] Finished at: 2021-06-23T16:00:25+08:00
[INFO] ------------------------------------------------------------------------

4. Return a JSON response in Jersey

A few JSON response endpoints in Jersey application, and MOXy will handle the object to/from JSON conversion.

JsonResource.java

package com.mkyong.json;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.json.JSONObject;

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

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

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

        // create a JSON string
        JSONObject json = new JSONObject();
        json.put("result", "Jersey JSON example using MOXy");

        // MUST json.toString(), else empty result or Unconsumed content
        // return Response.status(Response.Status.OK).entity(json).build();
        return Response.status(Response.Status.OK).entity(json.toString()).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, "mkyong"),
                new User(2, "zilap")
        );
    }

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

        JSONObject json = new JSONObject();
        json.put("status", "ok");
        return Response.status(Response.Status.CREATED).entity(json.toString()).build();

    }

}

Below is the User object.

User.java

package com.mkyong.json;

// optional, for json response, no need @XmlRootElement
// only needed if works with XML
// @XmlRootElement
public class User {

    private int id;
    String name;

    // getters, setters and constructors and etc
}

5. Start Jersey application

Starts Jetty HTTP server and deploys the Jersey application at http://localhost:8080.

P.S The static initialization is optional, and it is for the logging setup to redirect Jersey’s J.U.L logs to the SLF4J Logback implementation.

MainApp.java

package com.mkyong;

import com.mkyong.json.JsonResource;
import org.eclipse.jetty.server.Server;
import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.slf4j.bridge.SLF4JBridgeHandler;

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");

    // For logging, setup to redirect J.U.L to logback
    static {
        // JUL Root logger to the lowest level, so that bridge can intercept all j.u.l. logs
        Logger.getLogger("").setLevel(Level.FINEST);

        // Optionally remove existing handlers attached to j.u.l root logger
        // (since SLF4J 1.6.5)
        SLF4JBridgeHandler.removeHandlersForRootLogger();

        // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during
        // the initialization phase of your application
        SLF4JBridgeHandler.install();
    }

    // Starts Jetty HTTP server
    public static Server startHttpServer() {

        final ResourceConfig config = new ResourceConfig();

        // scan only one resource
        config.register(JsonResource.class);

        return JettyHttpContainerFactory.createServer(BASE_URI, config);

    }

    public static void main(String[] args) throws Exception {

        try {

            final Server server = startHttpServer();

            server.start();

            // shut down hook
            Runtime.getRuntime().addShutdownHook(
                    new Thread(() -> {
                        try {
                            server.stop();
                        } catch (Exception e) {
                            Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, e);
                        }
                    })
            );

            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 MOXy"}

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

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

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

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /json/create HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 28
>
* upload completely sent off: 28 out of 28 bytes
< HTTP/1.1 201 Created
< Date: Wed, 23 Jun 2021 06:48:53 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Server: Jetty(11.0.0)
<
{"status":"ok"}*

7. Custom Moxy Config

7.1 In the above demo, the JSON response is in compact mode, the default behavior of MOXy; We can create a newMoxyJsonConfig to configure the MOXy, for example, enable the MOXy’s JSON response in a pretty print format.

CustomMoxyConfig.java

package com.mkyong.json;

import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;
import org.glassfish.jersey.moxy.json.MoxyJsonConfig;

@Provider
public class CustomMoxyConfig implements ContextResolver<MoxyJsonConfig> {

    final MoxyJsonConfig moxyJsonConfig;

    public CustomMoxyConfig() {
        moxyJsonConfig = new MoxyJsonConfig();

        // pretty print
        moxyJsonConfig.setFormattedOutput(true);
    }

    @Override
    public MoxyJsonConfig getContext(Class<?> type) {
        return moxyJsonConfig;
    }

}

7.2 Register the custom MoxyJsonConfig in ResourceConfig.

MainApp.java

  // Starts Jetty HTTP server
  public static Server startHttpServer() {

      final ResourceConfig config = new ResourceConfig();

      // scan only one resource
      config.register(JsonResource.class);

      // only register if contains custom moxy config
      config.register(CustomMoxyConfig.class);

      return JettyHttpContainerFactory.createServer(BASE_URI, config);

  }

7.3 Restart the MainApp again, and now the JSON response is in a pretty print format.

Terminal

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

8. ExceptionMapper – Custom 404 JSON response in Jersey

Later, we will create a custom ExceptionMapper to intercept the default 404 error exception and return our custom JSON response.

8.1 Jersey 404 Not Found

Try access to some non-exists URL, and Jersey will return status 404 and empty response.

Terminal

$ curl http://localhost:8080/json/all/error  

We can use curl -v to display the request and response detail, for example, the response status code.

Terminal

$ curl -v http://localhost:8080/json/all/error

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /json/all/error HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Wed, 23 Jun 2021 07:18:01 GMT
< Content-Length: 0
< Server: Jetty(11.0.0)
<

8.2 ExceptionMapper and NotFoundException

Create a custom exception mapper of NotFoundException to intercept the 404 error.

CustomExceptionMapper.java

package com.mkyong.json;

import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import org.json.JSONObject;

@Provider
public class CustomNotFoundExceptionMapper
        implements ExceptionMapper<NotFoundException> {

    @Override
    public Response toResponse(NotFoundException exception) {

        JSONObject json = new JSONObject();
        json.put("error", "url not found!");
        return Response.status(Response.Status.NOT_FOUND)
                .entity(json.toString())
                .build();
    }

}

8.3 Register custom ExceptionMapper in ResourceConfig

MainApp.java

  // Starts Jetty HTTP server
  public static Server startHttpServer() {

      final ResourceConfig config = new ResourceConfig();

      // scan only one resource
      config.register(JsonResource.class);

      // only register if contains custom moxy config
      config.register(CustomMoxyConfig.class);

      // only register if contains custom exception mapper
      config.register(CustomNotFoundExceptionMapper.class);

      return JettyHttpContainerFactory.createServer(BASE_URI, config);

  }

8.3 DEMO with CustomNotFoundExceptionMapper

Restart the Jersey application and access some non exists URL again. Now, Jersey will return our custom JSON response.

Terminal

$ curl http://localhost:8080/json/all/error
{"error":"url not found!"}

$ curl -v http://localhost:8080/json/all/error
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /json/all/error HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Wed, 23 Jun 2021 07:38:23 GMT
< Transfer-Encoding: chunked
< Server: Jetty(11.0.0)
<
{"error":"url not found!"}

8.4 WebApplicationException

Suppose we are unsure of the exact type of Exception to intercept, and we try to intercept the JAX-RS WebApplicationException. In that case, it should match all errors thrown by the Jersey application.

In IDE, we can put a breakpoint inside toResponse and observe the Jersey application type of exception throws.

CustomExceptionMapper.java

package com.mkyong.json;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import org.json.JSONObject;

@Provider
public class CustomExceptionMapper
      implements ExceptionMapper<WebApplicationException> {

  @Override
  public Response toResponse(WebApplicationException exception) {

      JSONObject json = new JSONObject();
      json.put("error", exception.getMessage());
      //json.put("error", "some error");

      // 400
      return Response.status(Response.Status.BAD_REQUEST)
              .entity(json.toString())
              .build();
  }

}

Still unable to catch the exception? try implements ExceptionMapper<Exception>.

9. Unit test a Jersey JSON endpoints

Below are some tests for the above Jersey endpoints.

JsonResourceTest.java

package com.mkyong;

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.eclipse.jetty.server.Server;
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 Server 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() throws Exception {
      server.stop();
  }

  @Test
  public void testJson() throws JSONException {

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

      JSONAssert.assertEquals(expected, actual, false);

  }

  @Test
  public void testJsonName() throws JSONException {

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

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

      String expected = "{\"id\":1,\"name\":\"mkyong\"}";
      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\":\"mkyong\"},{\"id\":2,\"name\":\"zilap\"}]";
      JSONAssert.assertEquals(expected, actual, false);

  }

  @Test
  public void testJsonCreateOk() throws JSONException {

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

      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 testJsonCustomNotFound() throws JSONException {

      Response response = target.path("json/all/error")
              .request(MediaType.APPLICATION_JSON).buildGet().invoke();

      // ensure 404 status code
      assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());

      // test json response
      String actual = response.readEntity(String.class);
      String expected = "{\"error\":\"url not found!\"}";
      JSONAssert.assertEquals(expected, actual, false);

  }

}

Alternative, try Jackson
Jersey and JSON examples (Jackson)

10. Download Source Code

$ git clone https://github.com/mkyong/jax-rs

$ cd jax-rs/jersey/jersey-json-moxy/

$ mvn package

$ java -jar target/jersey-json-moxy.jar

11. References

About Author

author image
Founder of Mkyong.com, love Java and open source stuff. Follow him on Twitter. If you like my tutorials, consider make a donation to these charities.

Comments

Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments