Open API Game: Swagger Play


In this article I want to tell how to use the Swagger module for the Play Framework, with real-life examples. I will tell:

  1. How to fasten the latest version of Swagger-Play (Play module that allows you to use swagger-api annotations and generate documentation based on the OpenAPI specification) and how to configure swagger-ui (javascript library used to visualize generated documentation)
  2. I will describe the main annotations of Swagger-Core and talk about the features of their use for Scala
  3. I'll tell you how to work with data model classes correctly
  4. How to get around the generic type problem in Swagger, which does not know how to work with generics
  5. How to teach Swagger to understand ADT (algebraic data types)
  6. How to describe collections

The article will be interesting to everyone who uses the Play Framework on Scala and is going to automate the documentation of the API.

Add dependency


Having studied many sources on the Internet, I conclude that in order to make Swagger and Play Framework friends, you need to install the Swagger Play2 module.

Library address on github:

https://github.com/swagger-api/swagger-play

Add dependency:

libraryDependencies ++= Seq(
  "io.swagger" %% "swagger-play2" % "2.0.1-SNAPSHOT"
)

And here the problem arises:

At the time of this writing, the dependency was not being pulled from either the Maven-central or the Sonatype repositories.

In Maven-central, all found builds ended in Scala 2.12. In general, there was not a single assembled version for Scala 2.13.

I really hope that they will appear in the future.

Climbing the Sonatype-releases repository, I found the current fork of this library. Address on github:

https://github.com/iterable/swagger-play

So, we insert the dependency:

libraryDependencies ++= Seq(
  "com.iterable" %% "swagger-play" % "2.0.1"
)

Add the Sonatype repository:

resolvers += Resolver.sonatypeRepo("releases")

(Not necessary, since this assembly is Maven-central)

Now it remains to activate the module in the application.conf configuration file

play.modules.enabled += "play.modules.swagger.SwaggerModule"

as well as add a route to routes:

GET     /swagger.json           controllers.ApiHelpController.getResources

And the module is ready to go.

Now the Swagger Play module will generate a json file that can be viewed in a browser.

To fully enjoy Swagger's features, you also need to download the visualization library: swagger-ui. It provides a convenient graphical interface for reading the swagger.json file, as well as the ability to send rest requests to the server, providing an excellent alternative to Postman, Rest-client, and other similar tools.

So, add depending:

libraryDependencies += "org.webjars" % "swagger-ui" % "3.25.3"

In the controller, we create a method that redirects calls to the static library index.html file:

def redirectDocs: Action[AnyContent] = Action {
    Redirect(
       url = "/assets/lib/swagger-ui/index.html",
       queryStringParams = Map("url" -> Seq("/swagger.json")))
  }

Well, we prescribe the route in the routes file:

GET   /docs                   controllers.HomeController.redirectDocs()

Of course, you need to connect the webjars-play library. Add depending:

libraryDependencies +=  "org.webjars" %% "webjars-play" % "2.8.0"

And add the route to the routes file:

GET     /assets/*file               controllers.Assets.at(path="/public", file)

Provided that our application is running, we type in the browser

http: // localhost: 9000 / docs

and, if everything is done correctly, we get to the swagger page of our application:



The page does not yet contain data about our rest-api. In order to change this, you need to use annotations, which will be scanned by the Swagger-Play module.

Annotations


A detailed description of all swagger-api-core annotations can be found at:

https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.X

In my project I used the following annotations:

@Api - notes the controller class as a Swagger resource (for scanning)

@ApiImplicitParam - describes an "implicit" parameter (for example, specified in the request body)

@ApiImplicitParams - serves as a container for several @ApiImplicitParam annotations

@ApiModel - allows you to describe the

@ApiModelProperty data model - describes and interprets the

@ApiOperation data model class field - describes the controller method (probably the main annotation on this list)

@ApiParam- describes the request parameter specified explicitly (in the query string, for example)

@ApiResponse - describes the server response to the

@ApiResponses request - serves as a container for several @ApiResponse annotations . Usually includes additional answers (for example, when error codes occur). A successful response is usually described in the @ApiOperation annotation .

So, in order for Swagger to scan the controller class, you need to add the @Api annotation

@Api(value = «RestController», produces = «application/json»)
class RestController @Inject()(

This is enough for Swagger to find routes related to controller methods in the routes file and try to describe them.



But simply specifying the Swagger controller class is clearly not enough. Swagger is waiting for us with other annotations.

Why can't Swagger do this automatically? Because he has no idea how our classes are serialized. In this project I use uPickle, someone uses Circe, someone Play-JSON. Therefore, you must provide links to the received and issued classes.

Since the library used is written in Java, there are many nuances in the Scala project.

And the first thing you have to deal with is the syntax: nested annotations do not work.

For example, Java code:

@ApiResponses(value = {
      @ApiResponse(code = 400, message = "Invalid ID supplied"),
      @ApiResponse(code = 404, message = "Pet not found") })


In Scala it will look like this:

@ApiResponses(value = Array(
      new ApiResponse(code = 400, message = "Invalid ID supplied"),
      new ApiResponse(code = 404, message = "Pet not found") ))


Example 1


So, let's describe a controller method that looks for an entity in a database:

def find(id: String): Action[AnyContent] = 
    safeAction(AllowRead(DrillObj)).async { implicit request =>
      drillsDao.findById(UUID.fromString(id))
        .map(x => x.fold(NotFound(s"Drill with id=$id not found"))(x => 
            Ok(write(x)))).recover(errorsPf)
    }      


Using annotations, we can specify a description of the method, an input parameter obtained from the query string, and also responses from the server. If successful, the method will return an instance of the Drill class:

 @ApiOperation(
    value = " ",
    response = classOf[Drill]
  )
  @ApiResponses(value = Array(
    new ApiResponse(code = 404, message = "Drill with id=$id not found")
  ))
  def find(@ApiParam(value = "String rep of UUID, id ") id: String)=
    safeAction(AllowRead(DrillObj)).async { implicit request =>
      drillsDao.findById(UUID.fromString(id))
        .map(x => x.fold(NotFound(s"Drill with id=$id not found"))(x =>
          Ok(write(x)))).recover(errorsPf)
    }




We got a good description. Swagger almost guessed what the object was serialized with, with one exception: the start and end fields in our Drill class are Instant objects, and are serialized in Long. I would like to replace 0 with more suitable values. We can do this by applying the @ApiModel, @ApiModelProperty annotations to our class:

@ApiModel
case class Drill(
                id: UUID,
                name: String,
                @ApiModelProperty(
                  dataType = "Long",
                  example = "1585818000000"
                )
                start: Instant,
                @ApiModelProperty(
                  dataType = "Long",
                  example = "1585904400000"
                )
                end: Option[Instant],
                isActive: Boolean
                )


Now we have an absolutely correct description of the model:




Example 2


To describe the Post method, where the input parameter is passed in the request body, the @ApiImplicitParams annotation is used:

 @ApiOperation(value = " ")
  @ApiImplicitParams(Array(
    new ApiImplicitParam(
      value = " ",
      required = true,
      dataTypeClass = classOf[Drill],
      paramType = "body"
    )
  ))
  @ApiResponses(value = Array(
    new ApiResponse(code = 200, message = "ok")
  ))
  def insert() = safeAction(AllowWrite(DrillObj)).async { implicit request =>

Example 3


So far, everything was simple. Here is a more complex example. Suppose there is a generalized class depending on a type parameter:

case class SessionedResponse[T](
                            val ses: SessionData,
                            val payload: T
                          )

Swagger doesn't understand generics yet, at least. We cannot indicate in the annotation:

@ApiOperation(
    value = " ",
    response = classOf[SessionedResponse[Drill]]
  )


The only way in this situation is to subclass a generic type for each of the types we need. For example, we could subclass DrillSessionedResponse.
The only trouble is, we cannot inherit from the case class. Fortunately, in my project, nothing prevents me from changing the case class to class. Then:

class SessionedResponse[T](
                            val ses: SessionData,
                            val payload: T
                          )

object SessionedResponse {
  def apply[T](ses: SessionData, payload: T) = new SessionedResponse[T](ses, payload)
 
}

private[controllers] class DrillSessionedResponse(
          ses: SessionData,
          payload: List[Drill]
) extends SessionedResponse[List[Drill]](ses, payload)

Now I can specify this class in the annotation:

@ApiOperation(
    value = " ",
    response = classOf[DrillSessionedResponse]
  )

Example 4


Now an even more complex example related to ADT - algebraic data types.

The Swagger provides a mechanism for working with ADT:

Abstract @ApiModel has 2 options for this purpose:

1. subTypes - enumeration of subclasses

2. DISCRIMINATOR - field on which sub-classes differ from one another.

In my case, uPickle, producing JSON from case classes, adds the $ type field itself, and case - serializes objects into strings. So the approach with the discriminator field was not acceptable.

I used a different approach. Let's say there is

sealed trait Permission

case class Write(obj: Obj) extends Permission
case class Read(obj: Obj) extends Permission


where Obj is another ADT consisting of case objects:

//  permission.drill
case object DrillObj extends Obj

// permission.team
case object TeamObj extends Obj


In order for Swagger to understand this model, instead of a real class (or trait), it needs to provide a class specially created for this purpose with the necessary fields:

@ApiModel(value = "Permission")
case class FakePermission(
       @ApiModelProperty(
         name = "$type",
         allowableValues = "ru.myproject.shared.Read, ru.myproject.shared.Read"
       )
       t: String,
       @ApiModelProperty(allowableValues = "permission.drill, permission.team"
       obj: String
     )

Now we must specify FakePermission in the annotation instead of Permission

@ApiImplicitParams(Array(
    new ApiImplicitParam(
      value = "",
      required = true,
      dataTypeClass = classOf[FakePermission],
      paramType = "body"
    )
  ))

Collections


The last thing I wanted to draw the attention of readers. As I said, Swagger does not understand generic types. However, he knows how to work with collections.

So, the @ApiOperation annotation has a responseContainer parameter to which you can pass the value “List”.

Regarding the input parameters, an indication

dataType = "List[ru.myproject.shared.roles.FakePermission]"

within annotations that support this attribute, produces the desired results. Although, if you specify scala.collection.List - it does not work.

Conclusion


In my project, using Swagger-Core annotations, I was able to fully describe the Rest-API and all data models, including generic types and algebraic data types. In my opinion, using the Swagger-Play module is optimal for automatically generating API descriptions.

All Articles