Post

Building and Documenting a REST API with Jersey and Swagger

Setting up a JAX-RS REST API using Jersey, documenting it with Swagger, and the configuration that trips people up.

Building and Documenting a REST API with Jersey and Swagger

At some point on almost every Java project I’ve worked on, someone has asked for an API. And then immediately after the API is built, someone else has asked “but where’s the documentation?”

Jersey (the JAX-RS reference implementation) handles the API. Swagger handles the documentation. Wiring them together takes more configuration than you’d hope, but once it’s set up it maintains itself — the docs update whenever the code changes.

Project setup

Maven dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- Jersey -->
<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-server</artifactId>
    <version>2.40</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet</artifactId>
    <version>2.40</version>
</dependency>

<!-- Swagger (OpenAPI) -->
<dependency>
    <groupId>io.swagger.core.v3</groupId>
    <artifactId>swagger-jaxrs2</artifactId>
    <version>2.2.15</version>
</dependency>
<dependency>
    <groupId>io.swagger.core.v3</groupId>
    <artifactId>swagger-jaxrs2-servlet-initializer-v2</artifactId>
    <version>2.2.15</version>
</dependency>

JAX-RS Application class

1
2
3
4
5
6
7
8
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/api")
public class ApiApplication extends Application {
    // Jersey scans the classpath for @Path annotated classes
    // No need to register them manually if package scanning is configured
}

A resource endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.responses.*;

@Path("/payments")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PaymentResource {

    @GET
    @Path("/{id}")
    @Operation(
        summary = "Get payment by ID",
        description = "Returns a payment record by its unique identifier"
    )
    @APIResponse(
        responseCode = "200",
        description = "Payment found",
        content = @Content(schema = @Schema(implementation = PaymentResponse.class))
    )
    @APIResponse(responseCode = "404", description = "Payment not found")
    public Response getPayment(@PathParam("id") String paymentId) {
        PaymentResponse payment = paymentService.findById(paymentId);
        if (payment == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        return Response.ok(payment).build();
    }

    @POST
    @Operation(summary = "Submit a new payment")
    @APIResponse(
        responseCode = "201",
        description = "Payment submitted",
        content = @Content(schema = @Schema(implementation = PaymentResponse.class))
    )
    public Response submitPayment(
        @RequestBody(
            required = true,
            content = @Content(schema = @Schema(implementation = PaymentRequest.class))
        ) PaymentRequest request
    ) {
        PaymentResponse response = paymentService.submit(request);
        return Response.status(Response.Status.CREATED).entity(response).build();
    }
}

Swagger configuration

Add a openapi-configuration.json at the root of your classpath (or configure via @OpenAPIDefinition):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "resourcePackages": ["com.example.api.resources"],
  "prettyPrint": true,
  "openAPI": {
    "info": {
      "title": "Payments API",
      "description": "Internal payments processing API",
      "version": "1.0.0"
    },
    "servers": [
      {
        "url": "https://payments.example.com/api",
        "description": "Production"
      }
    ]
  }
}

With swagger-jaxrs2-servlet-initializer-v2 on the classpath, Swagger automatically registers a servlet that exposes the spec at /openapi.json and /openapi.yaml.

Annotating your model classes

Swagger generates schema definitions from your request/response classes. Add annotations to control what shows up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "Payment submission request")
public class PaymentRequest {

    @Schema(description = "Account to debit", example = "ACC001", required = true)
    private String fromAccount;

    @Schema(description = "Amount in minor units (cents)", example = "10050", required = true)
    private long amount;

    @Schema(description = "ISO 4217 currency code", example = "ZAR", required = true)
    private String currency;

    // getters/setters
}

Adding Swagger UI

Swagger UI is the browser-based interface that makes the documentation actually usable — you can read the docs and make test calls from the same page. Add the WebJar:

1
2
3
4
5
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>swagger-ui</artifactId>
    <version>5.4.2</version>
</dependency>

Then configure your servlet to serve it. With Jersey, add a web.xml entry or configure Jersey to serve static resources from the WebJar.

The UI will be available at /webjars/swagger-ui/index.html?url=/api/openapi.json.

What makes this worthwhile

The documentation can’t fall out of sync with the code because it’s generated from the code. When someone adds a new endpoint or changes a request field, the Swagger spec updates automatically on the next build.

In teams with multiple consumers of the API (other backend services, mobile apps, a third party), having a live spec that’s always accurate saves a lot of “but the docs say it should work like this” conversations.

This post is licensed under CC BY 4.0 by the author.