Jersey and JSON examples (EclipseLink 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
- 1. EclipseLink MOXy as the JSON provider in Jersey
- 2. Project directory
- 3. Project dependencies
- 4. Return a JSON response in Jersey
- 5. Start Jersey application
- 6. DEMO
- 7. Custom Moxy Config
- 8. ExceptionMapper – Custom 404 JSON response in Jersey
- 9. Unit test a Jersey JSON endpoints
- 10. Download Source Code
- 11. References
1. EclipseLink MOXy as the JSON provider in Jersey
We can include jersey-media-moxy to make EclipseLink MOXy the JSON provider in the Jersey application.
<!-- 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.
3. Project dependencies
Review the project dependencies.
<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>
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.
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.
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.
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.
$ 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.
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
.
// 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.
$ 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.
$ 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.
$ 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.
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
// 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.
$ 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.
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.
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
- Wikipedia JSON
- Eclipse Jersey
- EclipseLink MOXy
- Jersey and JSON support
- Jersey 3x Latest User Guide
- MOXy JSON Twitter example
- Specifying JSON Bindings
- Jersey and JSON examples (Jackson)
- cURL – POST request examples
- Jackson – How to parse JSON
- Java Logging APIs Tutorial
- JSONAssert – How to unit test JSON data