Showing posts with label openapi. Show all posts
Showing posts with label openapi. Show all posts

Sunday, February 24, 2019

The Hypermedia APIs support in JAX-RS and OpenAPI: a long way to go

Sooner or later, most of the developers who actively work on REST(ful) web services and APIs stumble upon this truly extraterrestrial thing called HATEOAS: Hypertext As The Engine Of Application State. The curiosity of what HATEOAS is and how it relates to REST would eventually lead to discovery of the Richardson Maturity Model which demystifies the industry definitions of REST and RESTful. The latter comes as an enlightenment, raising the question however: have we been doing REST wrong all these years?

Let us try to answer this question from the different perspectives. The HATEOAS is one of the core REST architectural constraints. From this perspective, the answer is "yes", in order to claim REST compliance, the web service or API should support that. Nonetheless, if you look around (or even consult your past or present experience), you may find out that the majority of the web services and APIs are just CRUD wrappers around the domain models, with no HATEOAS support whatsoever. Why is that? Probably, there is more than one reason, but from the developer's toolbox perspective, the backing of HATEOAS is not that great.

In today's post we are going to talk about what JAX-RS 2.x has to offer with respect to HATEOAS, how to use that from the server and client perspectives, and how to augment the OpenAPI v3.0.x specification to expose hypermedia as part of the contract. If you are excited, let us get started.

So our JAX-RS web APIs are going to be built around managing companies and their staff. The foundation is Spring Boot and Apache CXF, with Swagger as OpenAPI specification implementation. The AppConfig is the only piece of configuration we need to define in order to get the application up and running (thanks to Spring Boot auto-configuration capabilities).

@SpringBootConfiguration
public class AppConfig {
    @Bean
    OpenApiFeature createOpenApiFeature() {
        final OpenApiFeature openApiFeature = new OpenApiFeature();
        openApiFeature.setSwaggerUiConfig(new SwaggerUiConfig().url("/api/openapi.json"));
        return openApiFeature;
    }
    
    @Bean
    JacksonJsonProvider jacksonJsonProvider() {
        return new JacksonJsonProvider();
    }
}

The model is very simple, Company and Person (please notice that there is no direct relationships between these two classes, purposely).

public class Company {
    private String id;
    private String name;
}

public class Person {
    private String id;
    private String email;
    private String firstName;
    private String lastName;
}

This model is exposed through CompanyResource, a typical JAX-RS resource class annotated with @Path, and additionally with OpenAPI's @Tag annotation.

@Component
@Path( "/companies" ) 
@Tag(name = "companies")
public class CompanyResource {
    @Autowired private CompanyService service;
}

Great, the resource class has no endpoints defined yet, so let us beef it up. Our first endpoint would lookup the company by identifier and return its representation in JSON format. But since we do not incorporate any staff-related details, it would be awesome to hint the consumer (web UI or any other client) where to look it up. There are multiple ways to do that but since we stick to JAX-RS, we could use Web Linking (RFC-5988) which is supported out of the box. The code snippet is worth thousand words.

@Produces(MediaType.APPLICATION_JSON)
@GET
@Path("{id}")
public Response getCompanyById(@Context UriInfo uriInfo, @PathParam("id") String id) {
    return service
        .findCompanyById(id)
        .map(company -> Response
            .ok(company)
            .links(
                Link.fromUriBuilder(uriInfo
                        .getRequestUriBuilder())
                    .rel("self")
                    .build(),
                Link.fromUriBuilder(uriInfo
                        .getBaseUriBuilder()
                        .path(CompanyResource.class))
                    .rel("collection")
                    .build(),
                Link.fromUriBuilder(uriInfo
                       .getBaseUriBuilder()
                       .path(CompanyResource.class)
                       .path(CompanyResource.class, "getStaff"))
                    .rel("staff")
                    .build(id)
             )
            .build())
        .orElseThrow(() -> new NotFoundException("The company with id '" + id + "' does not exists"));
}

There are few things happening here. The one we care about is usage of the ResponseBuilder::links method where we supply three links. The first is self, which is essentially the link context (defined as part of RFC-5988). The second one, collection, is pointing out to the CompanyResource endpoint which returns the list of companies (also is included into standard relations registry). And lastly, the third one is our own staff relation which we assemble from another CompanyResource endpoint implemented by the method with the name getStaff (we are going to see it shortly). These links are going to be delivered in the Link response header and guide the client where to go next. Let us see it in action by running the application.

$ mvn clean package 
$ java -jar target/jax-rs-2.1-hateaos-0.0.1-SNAPSHOT.jar

And inspect the response from this resource endpoint using curl (the unnecessary details have been filtered out).

$ curl -v http://localhost:8080/api/companies/1
> GET /api/companies/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.1
> Accept: */*
>
< HTTP/1.1 200
< Link: <http://localhost:8080/api/companies/1>;rel="self"
< Link: <http://localhost:8080/api/companies/1/staff>;rel="staff"
< Link: <http://localhost:8080/api/companies>;rel="collection"
< Content-Type: application/json
< Transfer-Encoding: chunked
<
{
   "id":"1",
   "name":"HATEOAS, Inc."
}

The Link header is there, referring to other endpoints of interest. From the client perspective, the things are looking pretty straightforward as well. The Response class provides dedicated getLinks method to wrap around the access to Link response header, for example:

final Client client = ClientBuilder.newClient();

try (final Response response = client
        .target("http://localhost:8080/api/companies/{id}")
        .resolveTemplate("id", "1")
        .request()
        .accept(MediaType.APPLICATION_JSON)
        .get()) {
            
    final Optional staff = response
        .getLinks()
        .stream()
        .filter(link -> Objects.equals(link.getRel(), "staff"))
        .findFirst();
            
    staff.ifPresent(link -> {
        // follow the link here 
    });           
} finally {
    client.close();
}

So far so good. Moving forward, since HATEOAS is essentially a part of the web APIs contract, let us find out what OpenAPI specification has for it on the table. Unfortunately, HATEOAS is not supported as of now, but on the bright side, there is a notion of links (although they should not be confused with Web Linking, they are somewhat similar but not the same). To illustrate the usage of the links as part of the OpenAPI specification, let us decorate the endpoint with Swagger annotations.

@Operation(
    description = "Find Company by Id",
    responses = {
        @ApiResponse(
            content = @Content(schema = @Schema(implementation = Company.class)),
            links = {
                @io.swagger.v3.oas.annotations.links.Link(
                   name = "self", 
                   operationRef = "#/paths/~1companies~1{id}/get",
                   description = "Find Company",
                   parameters = @LinkParameter(name = "id", expression = "$response.body#/id")
                ),
                @io.swagger.v3.oas.annotations.links.Link(
                    name = "staff", 
                    operationRef = "#/paths/~1companies~1{id}~1staff/get",
                    description = "Get Company Staff",
                    parameters = @LinkParameter(name = "id", expression = "$response.body#/id")
                ),
                @io.swagger.v3.oas.annotations.links.Link(
                    name = "collection", 
                    operationRef = "#/paths/~1companies/get",
                    description = "List Companies"
                )
            },
            description = "Company details",
            responseCode = "200"
        ),
        @ApiResponse(
            description = "Company does not exist",
            responseCode = "404"
        )
    }
)
@Produces(MediaType.APPLICATION_JSON)
@GET
@Path("{id}")
public Response getCompanyById(@Context UriInfo uriInfo, @PathParam("id") String id) {
  // ...
}

If we run the application and navigate to the http://localhost:8080/api/api-docs in the browser (this is where Swagger UI is hosted), we would be able to see the links section along each response.

But besides that ... not much you could do with the links there (please watch for this issue if you are interested in the subject). The resource endpoint to get the company's staff is looking quite similar.

@Operation(
    description = "Get Company Staff",
    responses = {
        @ApiResponse(
            content = @Content(array = @ArraySchema(schema = @Schema(implementation = Person.class))),
            links = {
                @io.swagger.v3.oas.annotations.links.Link(
                    name = "self", 
                    operationRef = "#/paths/~1companies~1{id}~1staff/get",
                    description = "Staff",
                    parameters = @LinkParameter(name = "id", expression = "$response.body#/id")
                ),
                @io.swagger.v3.oas.annotations.links.Link(
                    name = "company", 
                    operationRef = "#/paths/~1companies~1{id}/get",
                    description = "Company",
                    parameters = @LinkParameter(name = "id", expression = "$response.body#/id")
                )
            },
            description = "The Staff of the Company",
            responseCode = "200"
        ),
        @ApiResponse(
            description = "Company does not exist",
            responseCode = "404"
        )
    }
)
@Produces(MediaType.APPLICATION_JSON)
@GET
@Path("{id}/staff")
public Response getStaff(@Context UriInfo uriInfo, @PathParam("id") String id) {
    return service
        .findCompanyById(id)
        .map(c -> service.getStaff(c))
        .map(staff -> Response
            .ok(staff)
            .links(
                Link.fromUriBuilder(uriInfo
                        .getRequestUriBuilder())
                    .rel("self")
                    .build(),
                Link.fromUriBuilder(uriInfo
                        .getBaseUriBuilder()
                        .path(CompanyResource.class)
                        .path(id))
                    .rel("company")
                    .build()
             )
            .build())
        .orElseThrow(() -> new NotFoundException("The company with id '" + id + "' does not exists"));
}

As you might expect, beside the link to self, it also includes the link to the company. When we try it out using curl, the expected response headers are returned back.

$ curl -v http://localhost:8080/api/companies/1/staff
> GET /api/companies/1/staff HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.1
> Accept: */*
>
< HTTP/1.1 200
< Link: <http://localhost:8080/api/companies/1/staff>;rel="self"
< Link: <http://localhost:8080/api/companies/1>;rel="company"
< Content-Type: application/json
< Transfer-Encoding: chunked
<
[
    {
        "id":"1",
        "email":"john@smith.com",
        "firstName":"John",
        "lastName":"Smith"
    },
    {
        "id":"2",
        "email":"bob@smith.com",
        "firstName":"Bob",
        "lastName":"Smith"
    }
]

So what kind of conclusions we can draw from that? HATEOAS indeed unifies the interaction model between web API providers and consumers by dynamically driving the conversations. This is very powerful, but most of the frameworks and tools out there either have pretty basic support of the HATEOAS (for example, Web Linking) or none at all.

There are many use cases when usage of the Web Linking is sufficient (the examples we have seen so far, paging, navigation, ...), but what about let say creating, editing or patching the existing resources? What about enriching with hypermedia the individual elements which are returned in the collection (described in RFC-6537)? Is HATEOAS worth all this efforts?

As always, the answer is "it depends", may be we should look beyond the JAX-RS? In the next post(s_ we are going to continue figuring things out.

The complete source code is available on Github.

Sunday, June 17, 2018

In the shoes of the consumer: do you really need to provide the client libraries for your APIs?

The beauty of the RESTful web services and APIs is that any consumer which speaks HTTP protocol will be able to understand and use it. Nonetheless, the same dilemma pops up over and over again: should you accompany your web APis with the client libraries or not? If yes, what languages or/and frameworks should you support?

Quite often this is not really an easy question to answer. So let us take a step back and think about the overall idea for a moment: what are the values which client libraries may bring to the consumers?

Someone may say to lower the barrier for adoption. Indeed, specifically in the case of strongly typed languages, exploring the API contracts from your favorite IDE (syntax highlighting and auto-completion please!) is quite handy. But by and large, the RESTful web APIs are simple enough to start with and good documentation would be certainly more valuable here.

Others may say it is good to shield the consumers from dealing with multiple API versions or rough edges. Also kind of make sense but I would argue it just hides the flaws with the way the web APIs in question are designed and evolve over time.

All in all, no matter how many clients you decide to bundle, the APIs are still going to be accessible by any generic HTTP consumer (curl, HttpClient, RestTemplate, you name it). Giving a choice is great but the price to pay for maintenance could be really high. Could we do it better? And as you may guess already, we certainly have quite a few options hence this post.

The key ingredient of the success here is to maintain an accurate specification of your RESTful web APIs, using OpenAPI v3.0 or even its predecessor, Swagger/OpenAPI 2.0 (or RAML, API Blueprint, does not really matter much). In case of OpenAPI/Swagger, the tooling is the king: one could use Swagger Codegen, a template-driven engine, to generate API clients (and even server stubs) in many different languages, and this is what we are going to talk about in this post.

To simplify the things, we are going to implement the consumer of the people management web API which we have built in the previous post. To begin with, we need to get its OpenAPI v3.0 specification in the YAML (or JSON) format.

java -jar server-openapi/target/server-openapi-0.0.1-SNAPSHOT.jar

And then:

wget http://localhost:8080/api/openapi.yaml

Awesome, the half of the job is done, literally. Now, let us allow Swagger Codegen to take a lead. In order to not complicate the matter, let's assume that consumer is also Java application, so we could understand the mechanics without any difficulties (but Java is only one of the options, the list of supported languages and frameworks is astonishing).

Along this post we are going to use OpenFeign, one of the most advanced Java HTTP client binders. Not only it is exceptionally simple to use, it offers quite a few integrations we are going to benefit from soon.

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-core</artifactId>
    <version>9.7.0</version>
</dependency>

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-jackson</artifactId>
    <version>9.7.0</version>
</dependency>

The Swagger Codegen could be run as stand-alone application from command-line, or Apache Maven plugin (the latter is what we are going to use).

<plugin>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-codegen-maven-plugin</artifactId>
    <version>3.0.0-rc1</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>/contract/openapi.yaml</inputSpec>
                <apiPackage>com.example.people.api</apiPackage>
                <language>java</language>
                <library>feign</library>
                <modelPackage>com.example.people.model</modelPackage>
                <generateApiDocumentation>false</generateApiDocumentation>
                <generateSupportingFiles>false</generateSupportingFiles>
                <generateApiTests>false</generateApiTests>
                <generateApiDocs>false</generateApiDocs>
                <addCompileSourceRoot>true</addCompileSourceRoot>
                <configOptions>
                    <sourceFolder>/</sourceFolder>
                    <java8>true</java8>
                    <dateLibrary>java8</dateLibrary>
                    <useTags>true</useTags>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

If some of the options are not very clear, the Swagger Codegen has pretty good documentation to look for clarifications. The important ones to pay attention to is language and library, which are set to java and feign respectively. The one thing to note though, the support of the OpenAPI v3.0 specification is mostly complete but you may encounter some issues nonetheless (as you noticed, the version is 3.0.0-rc1).

What you will get when your build finishes is the plain old Java interface, PeopleApi, annotated with OpenFeign annotations, which is direct projection of the people management web API specification (which comes from /contract/openapi.yaml). Please notice that all model classes are generated as well.

@javax.annotation.Generated(
    value = "io.swagger.codegen.languages.java.JavaClientCodegen",
    date = "2018-06-17T14:04:23.031Z[Etc/UTC]"
)
public interface PeopleApi extends ApiClient.Api {
    @RequestLine("POST /api/people")
    @Headers({"Content-Type: application/json", "Accept: application/json"})
    Person addPerson(Person body);

    @RequestLine("DELETE /api/people/{email}")
    @Headers({"Content-Type: application/json"})
    void deletePerson(@Param("email") String email);

    @RequestLine("GET /api/people/{email}")
    @Headers({"Accept: application/json"})
    Person findPerson(@Param("email") String email);

    @RequestLine("GET /api/people")
    @Headers({"Accept: application/json"})
    List<Person> getPeople();
}

Let us compare it with Swagger UI interpretation of the same specification, available at http://localhost:8080/api/api-docs?url=/api/openapi.json:

It looks right at the first glance but we have better ensure things work out as expected. Once we have OpenFeign-annotated interface, it could be made functional (in this case, implemented through proxies) using the family of Feign builders, for example:

final PeopleApi api = Feign
    .builder()
    .client(new OkHttpClient())
    .encoder(new JacksonEncoder())
    .decoder(new JacksonDecoder())
    .logLevel(Logger.Level.HEADERS)
    .options(new Request.Options(1000, 2000))
    .target(PeopleApi.class, "http://localhost:8080/");

Great, fluent builder style rocks. Assuming our people management web APIs server is up and running (by default, it is going to be available at http://localhost:8080/):

java -jar server-openapi/target/server-openapi-0.0.1-SNAPSHOT.jar

We could communicate with it by calling freshly built PeopleApi instance methods, as in the code snippet below.:

final Person person = api.addPerson(
        new Person()
            .email("a@b.com")
            .firstName("John")
            .lastName("Smith"));

It is really cool, if we rewind it back a bit, we actually did nothing. Everything is given to us for free with only web API specification available! But let us not stop here and remind ourselves that using Java interfaces will not eliminate the reality that we are dealing with remote systems. And things are going to fail here, sooner or later, no doubts.

Not so long ago we have learned about circuit breakers and how useful they are when properly applied in the context of distributed systems. It would be really awesome to somehow introduce this feature into our OpenFeign-based client. Please welcome another member of the family, HystrixFeign builder, the seamless integration with Hytrix library:

final PeopleApi api = HystrixFeign
    .builder()
    .client(new OkHttpClient())
    .encoder(new JacksonEncoder())
    .decoder(new JacksonDecoder())
    .logLevel(Logger.Level.HEADERS)
    .options(new Request.Options(1000, 2000))
    .target(PeopleApi.class, "http://localhost:8080/");

The only thing we need to do is just to add these two dependencies (strictly speaking hystrix-core is not really needed if you do not mind to stay on older version) to consumer's pom.xml file.

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hystrix</artifactId>
    <version>9.7.0</version>
</dependency>

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>1.5.12</version>
</dependency>

Arguably, this is one of the best examples of how easy and straightforward integration could be. But even that is not the end of the story. Observability in the distributed systems is as important as never and as we have learned a while ago, distributed tracing is tremendously useful in helping us out here. And again, OpenFeign has support for it right out of the box, let us take a look.

OpenFeign fully integrates with OpenTracing-compatible tracer. The Jaeger tracer is one of those, which among other things has really nice web UI front-end to explore traces and dependencies. Let us run it first, luckily it is fully Docker-ized.

docker run -d -e \
    COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
    -p 5775:5775/udp \
    -p 6831:6831/udp \
    -p 6832:6832/udp \
    -p 5778:5778 \
    -p 16686:16686 \
    -p 14268:14268 \
    -p 9411:9411 \
    jaegertracing/all-in-one:latest

A couple of additional dependencies have to be introduced in order to OpenFeign client to be aware of the OpenTracing capabilities.

<dependency>
    <groupId>io.github.openfeign.opentracing</groupId>
    <artifactId>feign-opentracing</artifactId>
    <version>0.1.0</version>
</dependency>

<dependency>
    <groupId>io.jaegertracing</groupId>
    <artifactId>jaeger-core</artifactId>
    <version>0.29.0</version>
</dependency>

From the Feign builder side, the only change (besides the introduction of the tracer instance) is to wrap up the client into TracingClient, like the snippet below demonstrates:

final Tracer tracer = new Configuration("consumer-openapi")
    .withSampler(
        new SamplerConfiguration()
            .withType(ConstSampler.TYPE)
            .withParam(new Float(1.0f)))
    .withReporter(
        new ReporterConfiguration()
            .withSender(
                new SenderConfiguration()
                    .withEndpoint("http://localhost:14268/api/traces")))
    .getTracer();
            
final PeopleApi api = Feign
    .builder()
    .client(new TracingClient(new OkHttpClient(), tracer))
    .encoder(new JacksonEncoder())
    .decoder(new JacksonDecoder())
    .logLevel(Logger.Level.HEADERS)
    .options(new Request.Options(1000, 2000))
    .target(PeopleApi.class, "http://localhost:8080/");

On the server-side we also need to integrate with OpenTracing as well. The Apache CXF has first-class support for it, bundled into cxf-integration-tracing-opentracing module. Let us include it as the dependency, this time to server's pom.xml.

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-integration-tracing-opentracing</artifactId>
    <version>3.2.4</version>
</dependency>

Depending on the way your configure your applications, there should be an instance of the tracer available which should be passed later on to the OpenTracingFeature, for example.

// Create tracer
final Tracer tracer = new Configuration(
        "server-openapi", 
        new SamplerConfiguration(ConstSampler.TYPE, 1),
        new ReporterConfiguration(new HttpSender("http://localhost:14268/api/traces"))
    ).getTracer();

// Include OpenTracingFeature feature
final JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();
factory.setProvider(new OpenTracingFeature(tracer()));
...
factory.create()

From now on, the invocation of the any people management API endpoint through generated OpenFeign client will be fully traceable in the Jaeger web UI, available at http://localhost:16686/search (assuming your Docker host is localhost).

Our scenario is pretty simple but imagine the real applications where dozen of external service calls could happen while the single request travels through the system. Without distributed tracing in place, every issue has a chance to turn into a mystery.

As a side note, if you look closer to the trace from the picture, you may notice that server and consumer use different versions of the Jaeger API. This is not a mistake since the latest released version of Apache CXF is using older OpenTracing API version (and as such, older Jaeger client API) but it does not prevent things to work as expected.

With that, it is time to wrap up. Hopefully, the benefits of contract-based (or even better, contract-first) development in the world of RESTful web services and APIs become more and more apparent: generation of the smart clients, consumer-driven contract test, discoverability and rich documentation are just a few to mention. Please make use of it!

The complete project sources are available on Github.

Monday, April 30, 2018

Moving With The Times: Towards OpenAPI v3.0.0 adoption in JAX-RS APIs

It is terrifying to see how fast time passes! The OpenAPI specification 3.0.0, a major revamp of so-get-used-to Swagger specification, has been released mostly one year ago but it took awhile for tooling to catch up. However, with the recent official release of the Swagger Core 2.0.0 things are going to accelerate for sure.

To prove the point, Apache CXF, the well-known JAX-RS 2.1 implementation, is one of the first adopters of the OpenAPI 3.0.0 and in today's post we are going to take a look how easy your JAX-RS 2.1 APIs could benefit from it.

As always, to keep things simple we are going to design a people management web APIs with just a handful set of resources to support it, nothing too exciting here.

POST   /api/people
GET    /api/people/{email}
GET    /api/people
DELETE /api/people/{email}

Our model would consist of a single Person class.

public class Person {
    private String email;
    private String firstName;
    private String lastName;
}

To add a bit of magic, we would be using Spring Boot to get us up and running as fast as possible. With that, let us start to fill in the dependencies (assuming we are using Apache Maven for build management).

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
    <version>3.2.4</version>
</dependency>

In the recent 3.2.x releases Apache CXF introduces a new module cxf-rt-rs-service-description-openapi-v3 dedicated to OpenAPI 3.0.0, based on Swagger Core 2.0.0.

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-rs-service-description-openapi-v3</artifactId>
    <version>3.2.4</version>
</dependency>

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>swagger-ui</artifactId>
    <version>3.13.6</version>
</dependency>

The presence of the Swagger UI is not strictly necessary, but this is exceptionally useful and beautiful tool to explore your APIs (and if it is available on classpath, Apache CXF seamlessly integrates it into your applications as we are going to see in a bit).

The prerequisites are in place, let us do some coding! Before we begin, it is worth to note that the Swagger Core 2.0.0 has many ways to populate the OpenAPI 3.0.0 definitions for your services, including property files, annotations or programmatically. Along this post we are going to use annotations only.

@OpenAPIDefinition(
    info = @Info(
        title = "People Management API",
        version = "0.0.1-SNAPSHOT",
        license = @License(
            name = "Apache 2.0 License",
            url = "http://www.apache.org/licenses/LICENSE-2.0.html"
        )
    )
)
@ApplicationPath("api")
public class JaxRsApiApplication extends Application {
}

Looks pretty simple, the @OpenAPIDefinition sets the top-level definition for all our web APIs. Moving on to the PeopleRestService, we just add the @Tag annotation to, well, tag our API.

@Path( "/people" ) 
@Tag(name = "people")
public class PeopleRestService {
    // ...
}

Awesome, nothing complicated so far. The meaty part starts with web API operation definitions, so let us take a look on the first example, the operation to fetch everyone.

@Produces(MediaType.APPLICATION_JSON)
@GET
@Operation(
    description = "List all people", 
    responses = {
        @ApiResponse(
            content = @Content(
                array = @ArraySchema(schema = @Schema(implementation = Person.class))
            ),
            responseCode = "200"
        )
    }
)
public Collection<Person> getPeople() {
    // ...
}

Quite a few annotations but by and large, looks pretty clean and straightforward. Let us take a look on another one, the endpoint to find the person by its e-mail address.

@Produces(MediaType.APPLICATION_JSON)
@Path("/{email}")
@GET
@Operation(
    description = "Find person by e-mail", 
    responses = {
        @ApiResponse(
            content = @Content(schema = @Schema(implementation = Person.class)), 
            responseCode = "200"
        ),
        @ApiResponse(
            responseCode = "404", 
            description = "Person with such e-mail doesn't exists"
        )
    }
)
public Person findPerson(
        @Parameter(description = "E-Mail address to lookup for", required = true) 
        @PathParam("email") final String email) {
    // ...
}

In the same vein, the operation to remove the person by e-mail looks mostly identical.

@Path("/{email}")
@DELETE
@Operation(
    description = "Delete existing person",
    responses = {
        @ApiResponse(
            responseCode = "204",
            description = "Person has been deleted"
        ),
        @ApiResponse(
            responseCode = "404", 
            description = "Person with such e-mail doesn't exists"
        )
     }
)
public Response deletePerson(
        @Parameter(description = "E-Mail address to lookup for", required = true ) 
        @PathParam("email") final String email) {
    // ...
}

Great, let us wrap up by looking into last, arguably the most interesting endpoint which adds a new person (the choice to use the @FormParam is purely to illustrate the different flavor of the APIs).

@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
@POST
@Operation(
    description = "Create new person",
    responses = {
        @ApiResponse(
            content = @Content(
                schema = @Schema(implementation = Person.class), 
                mediaType = MediaType.APPLICATION_JSON
            ),
            headers = @Header(name = "Location"),
            responseCode = "201"
        ),
        @ApiResponse(
            responseCode = "409", 
            description = "Person with such e-mail already exists"
        )
    }
)
public Response addPerson(@Context final UriInfo uriInfo,
        @Parameter(description = "E-Mail", required = true) 
        @FormParam("email") final String email, 
        @Parameter(description = "First Name", required = true) 
        @FormParam("firstName") final String firstName, 
        @Parameter(description = "Last Name", required = true) 
        @FormParam("lastName") final String lastName) {
    // ...
}

If you have an experience with documenting web APIs using older Swagger specifications, you might find the approach quite familiar but more verbose (or better to say, formalized). It is the result of the tremendous work which specification leads and community has done to make it as complete and extensible as possible.

The APIs are defined and documented, it is time to try them out! The missing piece though is the Spring configuration where we would initialize and expose our JAX-RS web services.

@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackageClasses = PeopleRestService.class)
public class AppConfig {
    @Autowired private PeopleRestService peopleRestService;
 
    @Bean(destroyMethod = "destroy")
    public Server jaxRsServer(Bus bus) {
        final JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();

        factory.setApplication(new JaxRsApiApplication());
        factory.setServiceBean(peopleRestService);
        factory.setProvider(new JacksonJsonProvider());
        factory.setFeatures(Arrays.asList(new OpenApiFeature()));
        factory.setBus(bus);
        factory.setAddress("/");

        return factory.create();
    }

    @Bean
    public ServletRegistrationBean cxfServlet() {
        final ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new CXFServlet(), "/api/*");
        servletRegistrationBean.setLoadOnStartup(1);
        return servletRegistrationBean;
    }
}

The OpenApiFeature is a key ingredient here which takes care of all the integration and introspection. The Spring Boot application is the last touch to finish the picture.

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}

Let us build and run it right away:

mvn clean package 
java -jar target/jax-rs-2.1-openapi-0.0.1-SNAPSHOT.jar

Having the application started, the OpenAPI 3.0.0 specification of our web APIs should be live and available for consumption in the JSON format at:

http://localhost:8080/api/openapi.json

Or in YAML format at:

http://localhost:8080/api/openapi.json

Wouldn't it be great to explore our web APIs and play with it? Since we included Swagger UI dependency, this is no brainer, just navigate to http://localhost:8080/api/api-docs?url=/api/openapi.json:

A special attention to be made to the small icon Swagger UI places alongside your API version, hinting about its conformity to OpenAPI 3.0.0 specification.

Just to note here, there is nothing special in using Spring Boot. In case you are using Apache CXF inside the OSGi container (like Apache Karaf for example), the integration with OpenAPI 3.0.0 is also available (please check out the official documentation and samples if you are interested in the subject).

It all looks easy and simple, but what about migrating to OpenAPI 3.0.0 from older versions of the Swagger specifications? The Apache CXF has an powerful feature to convert older the specifications on the fly but in general the OpenApi.Tools portal is a right place to evaluate your options.

Should you migrate to OpenAPI 3.0.0? I honestly believe you should, at least should try experimenting with it, but please be aware that the tooling is still not mature enough, expect a few roadblocks along the way (which you would be able to overcome by contributing the patches, by the way). But undoubtedly, the future is bright!

The complete project sources are available on Github.