Documentando uma API JAX-RS com Swagger

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:

public class Transacao {
  private String numero;
  private String titular;
  private LocalDate data;
  private BigDecimal valor;

 //getters e setters...
}
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:

@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:

<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:

@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
e
http://localhost:8080/fj36-payfast/v1/swagger.yaml

O JSON e o YAML gerados pelo Swagger a partir do nosso recurso PagamentoResource usam a linguagem OpenAPI para detalhar URIs, Content-Types, métodos HTTP aceitos, códigos de resposta e modelos de dados. O YAML gerado ficaria assim:

---
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.

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.

@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:

  /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.

url = "http://petstore.swagger.io/v2/swagger.json";

Devemos mudar para:

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!

Tags: , , ,

8 Comentários

  1. Lazaro 22/03/2016 at 16:39 #

    Ótimo post. Tenho usado o Swagger junto com o JHipster e tenho gostado bastante.

    (gostaria de um exemplo desse com o nosso famoso VRaptor, que é prata da casa)

  2. Alexandre Aquiles 22/03/2016 at 16:48 #

    Boa sugestão, Lazaro!

  3. Everton Agner Ramos 22/03/2016 at 18:11 #

    Caramba! Implementei manualmente uma funcionalidade parecida recentemente e agora fiquei surpreso por existir uma solução que fizesse isso naturalmente. Obrigado e parabéns pelo post!

  4. Tiago 23/03/2016 at 20:05 #

    É uma excelente ferramenta, estamos começando a utilizar em nossos projetos. Um recurso que a galera aqui tem usado muito é o “Try it out!” que permite passar parâmetros e executar a API que foi documentada diretamente na página da documentação.

  5. Alexandre Aquiles 23/03/2016 at 21:09 #

    Boa dica, Tiago!

  6. Luciano Silveira 24/03/2016 at 10:51 #

    Em um projeto nosso decidimos não utilizar o JSF optando para uma interface feita com Angular e backend em Rest.

    Como a mesma equipe que desenvolvia o backend também tocava a camada de visão não se preocuparam em documentar a interface.

    A necessidade surgiu quando decidimos realizar testes automatizados, que aqui na empresa é feita por outra equipe, utilizando diretamente a API Rest para testes subcutâneos.

    A forma menos custosa para gerar a documentação foi a utilização desta ferramenta para geração automática:

    https://github.com/sdaschner/jaxrs-analyzer/

    A cada build gerada a documentação é atualizada e pode ser consultada utilizando o Swagger-UI.

  7. Alexandre Aquiles 24/03/2016 at 11:26 #

    Muito bom, Luciano! Excelente maneira de gerar a documentação a cada build do seu projeto!

Deixe uma resposta