Documentando uma API JAX-RS com Swagger

Documentando uma API JAX-RS com Swagger
alexandre.aquiles
alexandre.aquiles

Compartilhe

Em um post anterior, falamos sobre como modelar uma API REST com o Swagger. Essa abordagem, em que pensamos nos detalhes da API antes da implementá-la, é conhecida como Contract-First ou API-First.

Uma outra abordagem, talvez mais comum, é iniciar pela implementação da API para só então documentá-la. Essa abordagem é conhecida como Contract-Last.

Vamos utilizar o Swagger para documentar uma API REST tanto para máquinas quanto para humanos.

Lembrando da API do Payfast

Documentaremos a API do Payfast, uma API de pagamentos simplificada que implementamos no curso SOA na prática, utilizando JAX-RS.

Entre as classes de modelo, temos Transacao e Pagamento: ```java public class Transacao { private String numero; private String titular; private LocalDate data; private BigDecimal valor;

//getters e setters... }


```java
 public class Pagamento { private Integer id; private String status; private BigDecimal valor;

//getters e setters... } 

Na nossa API REST, um POST em /pagamentos cria um novo pagamento a partir de uma transação. Então é possível confirmar o pagamento com um PUT ou cancelá-lo com um DELETE.

Máquina de estados da API do Payfast

Uma implementação dessa API utilizando JAX-RS seria algo como:

 @Path("/pagamentos") public class PagamentoResource { //... @POST @Consumes(MediaType.APPLICATION\_JSON) public Response criarPagamento(Transacao transacao) throws URISyntaxException { //aqui, código que cria pagamento a partir da transacao... return Response.created(new URI("/pagamentos/" + pagamento.getId())).entity(pagamento) .type(MediaType.APPLICATION\_JSON).build(); }

@PUT @Path("/{id}") @Produces(MediaType.APPLICATION\_JSON) public Response confirmarPagamento(@PathParam("id") Integer id) { //aqui, código que confirma pagamento... return Response.ok().entity(pagamento).build(); }

@DELETE @Path("/{id}") @Produces(MediaType.APPLICATION\_JSON) public Response cancelarPagamento(@PathParam("id") Integer id) { //aqui, código que cancela pagamento... return Response.ok().entity(pagamento).build(); } } 

Nota: Para que a serialização de/para JSON funcione para o java.time.LocalDate da classe Transacao em um servidor Java EE 7, são necessárias algumas configurações. Fazemos isso na classe JacksonJavaTimeConfiguration.

Não podemos esquecer de habilitar o JAX-RS na nossa aplicação: ```java @ApplicationPath("/v1") public class PagamentoService extends Application { }


# Configurando o Swagger na sua API

Para usar o Swagger para documentar nossa API, precisamos de seus jars. A melhor maneira é utilizar alguma ferramenta de gerenciamento de dependências. Para configurar a dependência com o Maven: ```xml
 <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-jaxrs</artifactId> <version>1.5.7</version> </dependency> 

Nota: Pode ser interessante excluir algumas dependências transitivas do swagger-jaxrs.

Por meio da classe BeanConfig, do pacote io.swagger.jaxrs.config, fazemos configurações básicas como título, descrição e versão da API, endereço do servidor e contexto da aplicação, se é usado HTTP ou HTTPS e pacotes cujos recursos REST devem ser escaneados. Criamos um objeto dessa classe no construtor de PagamentoService e ao invocar o método setScan, as configurações são realizadas.

 @ApplicationPath("/v1") public class PagamentoService extends Application { public PagamentoService() { BeanConfig conf = new BeanConfig(); conf.setTitle("Payfast API"); conf.setDescription("Pagamentos rápidos"); conf.setVersion("1.0.0"); conf.setHost("localhost:8080"); conf.setBasePath("/fj36-payfast/v1"); conf.setSchemes(new String\[\] { "http" }); conf.setResourcePackage("br.com.caelum.payfast"); conf.setScan(true); } } 

Além disso, precisamos carregar as classes ApiListingResource e SwaggerSerializers do Swagger. Para isso, sobrescrevemos o método getClasses de Application, em PagamentoService. Uma coisa chata é que, ao fazermos isso, precisamos adicionar manualmente a classe JacksonJavaTimeConfiguration e o recurso PagamentoResource.

 @ApplicationPath("/v1") public class PagamentoService extends Application { public PagamentoService() { //código omitido... } @Override public Set<Class<?>> getClasses() { Set<Class<?>> resources = new HashSet<>(); resources.add(JacksonJavaTimeConfiguration.class); resources.add(PagamentoResource.class); //classes do swagger... resources.add(ApiListingResource.class); resources.add(SwaggerSerializers.class); return resources; } } 

Para que o Swagger gere a documentação para o nosso recurso, devemos anotar a classe PagamentoResource com @Api: ```code language="java" highlight="1" @Api @Path("/pagamentos") public class PagamentoResource { //restante do código... }


# Obtendo a documentação gerada pelo Swagger

Então, podemos obter o JSON e YAML que descrevem nossa API nas seguintes URLs, respectivamente: [http://localhost:8080/fj36-payfast/v1/swagger.json](http://swagger-payfast.7e14.starter-us-west-2.openshiftapps.com/v1/swagger.json) e [http://localhost:8080/fj36-payfast/v1/swagger.yaml](http://swagger-payfast.7e14.starter-us-west-2.openshiftapps.com/v1/swagger.yaml)

O JSON e o YAML gerados pelo Swagger a partir do nosso recurso `PagamentoResource` usam a linguagem [OpenAPI](https://openapis.org/specification) para detalhar URIs, Content-Types, métodos HTTP aceitos, códigos de resposta e modelos de dados. O YAML gerado ficaria assim: ```ruby
 --- swagger: "2.0" info: description: "Pagamentos rápidos" version: "1.0.0" title: "Payfast API" host: "localhost:8080" basePath: "/fj36-payfast/v1" schemes: - "http" paths: /pagamentos: post: operationId: "criarPagamento" parameters: - in: "body" name: "body" required: false schema: $ref: "#/definitions/Transacao" responses: default: description: "successful operation" /pagamentos/{id}: put: operationId: "confirmarPagamento" parameters: - name: "id" in: "path" required: true type: "integer" format: "int32" responses: default: description: "successful operation" delete: operationId: "cancelarPagamento" parameters: - name: "id" in: "path" required: true type: "integer" format: "int32" responses: default: description: "successful operation" definitions: Transacao: type: "object" properties: numero: type: "string" titular: type: "string" data: type: "string" format: "date" valor: type: "number" Pagamento: type: "object" properties: id: type: "integer" format: "int32" status: type: "string" valor: type: "number" 

Bem parecido com o que fizemos no post anterior.

Banner da Escola de Programação: Matricula-se na escola de Programação. Junte-se a uma comunidade de mais de 500 mil estudantes. Na Alura você tem acesso a todos os cursos em uma única assinatura; tem novos lançamentos a cada semana; desafios práticos. Clique e saiba mais!

Corrigindo alguns detalhes

Há algumas diferenças entre o YAML modelado para a nossa API no post anterior e o YAML gerado pelo Swagger a partir de PagamentoResource.

Para o POST em /pagamentos, as diferenças mais relevantes são:

  • Não há uma descrição (summary)
  • Sumiram os content-types recebidos e enviados
  • O response ficou com uma descrição genérica, sem o cabeçalho de Location nem o status 201
  • O parâmetro está não obrigatório e com um nome ruim (body)

Para definir uma descrição e content-types para o POST em /pagamentos, devemos utilizar a anotação @ApiOperation. Para o status e cabeçalho no response, usamos as anotações @ApiResponses, @ApiResponse e @ResponseHeader. Para modificar a obrigatoriedade e nome do parâmetro, usamos @ApiParam. ```code language="java" highlight="1-14,18-21" @ApiOperation( value = "Cria novo pagamento", consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON) @ApiResponses( @ApiResponse( code=201, message="Novo pagamento criado", response = Pagamento.class, responseHeaders= @ResponseHeader( name="Location", description="uri do novo pagamento", response=String.class))) @POST @Consumes(MediaType.APPLICATION_JSON) public Response criarPagamento( @ApiParam( value="Transação", name="transacao", required=true) Transacao transacao) throws URISyntaxException { //código omitido... }


Após essas configurações, o trecho do POST no YAML passaria a ser gerado assim: ```code language="ruby" highlight="3,6-9,12-14,18-25"
 /pagamentos: post: summary: "Cria novo pagamento" description: "" operationId: "criarPagamento" consumes: - "application/json" produces: - "application/json" parameters: - in: "body" name: "transacao" description: "Transação" required: true schema: $ref: "#/definitions/Transacao" responses: 201: description: "Novo pagamento criado" schema: $ref: "#/definitions/Pagamento" headers: Location: type: "string" description: "uri do novo pagamento" 

Bem parecido com o que tinhamos antes!

As diferenças mais importantes para o PUT e o DELETE são as seguintes:

  • Não há o status 200 no response
  • O path parameter está duplicado

Podemos usar a anotação @ApiResponse para melhorar as informações do response. Infelizmente, não há como evitar a duplicação do parâmetro id.

 @ApiResponses( @ApiResponse( code=200, message="Pagamento confirmado", response = Pagamento.class)) @PUT @Path("/{id}") @Produces(MediaType.APPLICATION\_JSON) public Response confirmarPagamento(@PathParam("id") Integer pagamentoId) { //código omitido... }

@ApiResponses( @ApiResponse( code=200, message="Pagamento cancelado", response = Pagamento.class)) @DELETE @Path("/{id}") @Produces(MediaType.APPLICATION\_JSON) public Response cancelarPagamento(@PathParam("id") Integer pagamentoId) { //código omitido... } 

O trecho correspondente do YAML ficaria:

 /pagamentos/{id}: put: operationId: "confirmarPagamento" parameters: - name: "id" in: "path" required: true type: "integer" format: "int32" responses: 200: description: "Pagamento confirmado" schema: $ref: "#/definitions/Pagamento" delete: operationId: "cancelarPagamento" parameters: - name: "id" in: "path" required: true type: "integer" format: "int32" responses: 200: description: "Pagamento cancelado" schema: $ref: "#/definitions/Pagamento" 

Documentação para humanos

Temos o JSON e o YAML, mas esses formatos não são legíveis para pessoas. São quase tão difíceis de ler quanto um WSDL.

Em Web Services do estilo SOAP, é bastante comum que sejam disponibilizados PDFs que focam em descrever a API para humanos. É o caso dos Correios e da Nota Fiscal Paulista, por exemplo.

Será que precisamos escrever um PDF manualmente? Seria interessante alguma maneira de transformar esse JSON ou YAML em uma página HTML com boa usabilidade.

Para isso, há o projeto Swagger UI! Na página da ferramenta, podemos baixar um zip com o último release. Depois de extrair, basta copiar o conteúdo da pasta dist para uma pasta doc no document root folder (WebContent ou webapp) do nosso projeto.

Então, a página da documentação estaria acessível em: http://localhost:8080/fj36-payfast/doc/

O estranho é que a documentação exibida não seria da nossa API, mas de uma tal de PetStore. É um exemplo que vem configurado no Swagger UI.

Precisamos modificar o seguinte trecho de JavaScript da página index.html, corrigindo a URL. ```javascript url = "http://petstore.swagger.io/v2/swagger.json";


Devemos mudar para: ```javascript
 url = "../v1/swagger.json"; 

Ao acessar novamente a página, teríamos uma documentação razoavelmente legível: Exemplo de Swagger UI para a Payfast API

O Swagger UI, através de um código JavaScript, transforma o swagger.json nessa documentação. É possível disparar requisições de teste com diferentes content-type. É possível utilizar autorização, oauth, entre várias outras funcionalidades.

O código desse post pode ser encontrado em: https://github.com/alexandreaquiles/fj36-payfast-swagger

E você? Já usou o Swagger em algum projeto para documentar uma API existente, no estilo Contract-Last? Conte-nos como foi a experiência!

Veja outros artigos sobre Programação