You are viewing the documentation for the 2.7.8 release in the 2.7.x series of releases. The latest stable release series is 3.0.x.

§What’s new in Play 2.7

This page highlights the new features of Play 2.7. If you want to learn about the changes you need to make when you migrate to Play 2.7, check out the Play 2.7 Migration Guide.

§Scala 2.13 support

Play 2.7 is the first release of Play that is cross-built against Scala 2.13.0, 2.12, and 2.11. A number of dependencies were updated to achieve this.

You can select which version of Scala you would like to use by setting the scalaVersion setting in your build.sbt.

For Scala 2.12:

scalaVersion := "2.12.9"

For Scala 2.11:

scalaVersion := "2.11.12"

For Scala 2.13.0:

scalaVersion := "2.13.0"

§Lifecycle managed by Akka’s Coordinated Shutdown

Play 2.6 introduced the usage of Akka’s Coordinated Shutdown but still didn’t use it all across the core framework or exposed it to the end user. Coordinated Shutdown is an Akka Extension with a registry of tasks that can be run in an ordered fashion during the shutdown of the Actor System.

Coordinated Shutdown internally handles Play 2.7 Play’s lifecycle and an instance of CoordinatedShutdown is available for injection. Coordinated Shutdown gives you fine grained phases - organized as a directed acyclic graph (DAG) - where you can register tasks instead of just having a single phase like Play’s application lifecycle. For example, you can add tasks to run before or after server binding, or after all the current requests finishes. Also, you will have better integration with Akka Cluster.

You can find more details on the new section on Coordinated Shutdown on the Play manual, or you can have a look at Akka’s reference docs on Coordinated Shutdown.

§Guice was upgraded to 4.2.2

Guice, the default dependency injection framework used by Play, was upgraded to 4.2.2 (from 4.1.0). Have a look at the 4.2.2, 4.2.1 and the 4.2.0 release notes. This new Guice version introduces breaking changes, so make sure you check the Play 2.7 Migration Guide.

§Java forms bind multipart/form-data file uploads

Until Play 2.6, the only way to retrieve a file that was uploaded via a multipart/form-data encoded form was by calling request.body().asMultipartFormData().getFile(...) inside the action method.

Starting with Play 2.7 such an uploaded file will now also be bound to a Java Form. If you are not using a custom multipart file part body parser all you need to do is add a FilePart of type TemporaryFile to your form:

import play.libs.Files.TemporaryFile;
import play.mvc.Http.MultipartFormData.FilePart;

public class MyForm {

  private FilePart<TemporaryFile> myFile;
  public void setMyFile(final FilePart<TemporaryFile> myFile) {
    this.myFile = myFile;

  public FilePart<TemporaryFile> getMyFile() {
    return this.myFile;

Like before, use the FormFactory you injected into your Controller to create the form:

Form<MyForm> form = formFactory.form(MyForm.class).bindFromRequest(req);

If the binding was successful (form validation passed) you can access the file:

MyForm myform = form.get();

Some useful methods were added as well to work with uploaded files:

// Get all files of the form

// Access the file of a Field instance
Field myFile = form.field("myFile");

// To access a file of a DynamicForm instance

Note: If you are using using a custom multipart file part body parser you just have to replace TemporaryFile with the type your body parser uses.

§Constraint annotations offered for Play Java are now @Repeatable

All of the constraint annotations defined by are now @Repeatable. This change lets you, for example, reuse the same annotation on the same element several times but each time with different groups. For some constraints however it makes sense to let them repeat itself anyway, like @ValidateWith:

public class MyForm {

    @Pattern(value="[a-k]", message="Should be a - k")
    @Pattern(value="[c-v]", message="Should be c - v")
    @MinLength(value=4, groups={GroupA.class})
    @MinLength(value=7, groups={GroupB.class})
    private String name;


You can of course also make your own custom constraints @Repeatable as well and Play will automatically recognize that.

§Payloads for Java validate and isValid methods

When using advanced validation features you can now pass a ValidationPayload object, containing useful information sometimes needed for a validation process, to a Java validate or isValid method.
To pass such a payload to a validate method just annotate your form with @ValidateWithPayload (instead of just @Validate) and implement ValidatableWithPayload (instead of just Validatable):

import java.util.Map;
import com.typesafe.config.Config;
import play.i18n.Lang;
import play.i18n.Messages;
import play.libs.typedmap.TypedMap;

public class SomeForm implements ValidatableWithPayload<String> {

    public String validate(ValidationPayload payload) {
        Lang lang = payload.getLang();
        Messages messages = payload.getMessages();
        Map<String, Object> ctxArgs = payload.getArgs();
        TypedMap attrs = payload.getAttrs();
        Config config = payload.getConfig();
        // ...


In case you wrote your own custom class-level constraint, you can also pass a payload to an isValid method by implementing PlayConstraintValidatorWithPayload (instead of just PlayConstraintValidator):

import javax.validation.ConstraintValidatorContext;

// ...

public class ValidateWithDBValidator implements PlayConstraintValidatorWithPayload<SomeValidatorAnnotation, SomeValidatableInterface<?>> {


    public boolean isValid(final SomeValidatableInterface<?> value, final ValidationPayload payload, final ConstraintValidatorContext constraintValidatorContext) {
        // You can now pass the payload on to your custom validate(...) method:
        return reportValidationStatus(value.validate(...., payload), constraintValidatorContext);


Note: Don’t get confused with ValidationPayload and ConstraintValidatorContext: The former class is provided by Play and is what you use in your day-to-day work when dealing with forms in Play. The latter class is defined by the Bean Validation specification and is used only internally in Play - with one exception: This class emerges when your write your own custom class-level constraints, like in the last example above, where you only need to pass it on to the reportValidationStatus method however anyway.

§Support for Caffeine

Play now offers a CacheApi implementation based on Caffeine. Caffeine is the recommended cache implementation for Play users.

To migrate from EhCache to Caffeine you will have to remove ehcache from your dependencies and replace it with caffeine. To customize the settings from the defaults, you will also need to update the configuration in application.conf as explained in the documentation.

Read the documentation for the Java cache API and Scala cache API to learn more about configuring caching with Play.

§New Content Security Policy Filter

There is a new Content Security Policy filter available that supports CSP nonce and hashes for embedded content.

The previous setting of enabling CSP by default and setting it to default-src 'self' was too strict, and interfered with plugins. The CSP filter is not enabled by default, and the contentSecurityPolicy in the SecurityHeaders filter is now deprecated and set to null by default.

The CSP filter uses Google’s Strict CSP policy by default, which is a nonce based policy. It is recommended to use this as a starting point, and use the included CSPReport body parsers and actions to log CSP violations before enforcing CSP in production.

§HikariCP upgraded

HikariCP was updated to its latest major version. Have a look at the Migration Guide to see what changed.

§Play WS curl filter for Java

Play WS enables you to create to inspect or enrich the requests made. Play provides a “log as curl” filter, but this was lacking for Java developers. You can now write something like:

  .setRequestFilter(new AhcCurlRequestLogger())
  .addHeader("My-Header", "Header value")

And then the following log will be printed:

curl \
  --verbose \
  --request GET \
  --header 'My-Header: Header Value' \

This can be specially useful if you want to reproduce the request in isolation and also change curl parameters to see how it goes.

§Gzip Filter now supports compression level configuration

When using gzip encoding, you can now configure the compression level to use. You can configure it using play.filters.gzip.compressionLevel, for example:

play.filters.gzip.compressionLevel = 9

See more details at GzipEncoding.

§API Additions

Here are some of the relevant API additions we made for Play 2.7.0.

§Result HttpEntity streamed methods

Previous versions of Play had convenient methods to stream results using HTTP chunked transfer encoding:

public Result chunked() {
  Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second")));
  return ok().chunked(body);
def chunked = Action {
  val body = Source(List("first", "second", "..."))

In Play 2.6, there was no convenient method to return a streamed Result in the same way without using HTTP chunked encoding. You instead had to write this:

public Result streamed() {
  Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second")));
  return ok().sendEntity(new HttpEntity.Streamed(body, Optional.empty(), Optional.empty()));
def streamed = Action {
  val body = Source(List("first", "second", "...")).map(s => ByteString.fromString(s))
  Ok.sendEntity(HttpEntity.Streamed(body, None, None))

Play 2.7 fixes this by adding a new streamed method on results, that works similar to chunked:

public Result streamed() {
  Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second")));
  return ok().streamed(body, Optional.empty(), Optional.empty());
def streamed = Action {
  val body = Source(List("first", "second", "...")).map(s => ByteString.fromString(s))
  Ok.streamed(body, contentLength = None)

§New Http Error Handlers

Play 2.7 brings two new implementations for play.api.http.HttpErrorHandler. The first one is JsonHttpErrorHandler, which will return errors formatted in JSON and is a better alternative if you are developing an REST API that accepts and returns JSON payloads. The second one is HtmlOrJsonHttpErrorHandler which returns HTML or JSON errors based on the preferences specified in client’s Accept header. It is a better option if your application uses a mixture of HTML and JSON, as is common in modern web apps.

You can read more details at the docs for Java or Scala.

§Nicer syntax for Router.withPrefix

In Play 2.7 we introduce some syntax sugar to use play.api.routing.Router.withPrefix. Instead of writing:

val router = apiRouter.withPrefix("/api")

You can now write:

val router = "/api" /: apiRouter

Or even combine more path segments:

val router = "/api" /: "v1" /: apiRouter

§Concatenating Routers

In Play 2.7 we introduce a new method orElse to programatically compose Routers.
You can now compose routers as following:

Router router = oneRouter.orElse(anotherRouter)
val router = oneRouter.orElse(anotherRouter)

§Isolation level for Database transactions

You can now choose an isolation level when using play.api.db.Database.withTransaction API (play.db.Database for Java users). For example:

public void someDatabaseOperation() {
  database.withTransaction(TransactionIsolationLevel.ReadUncommitted, connection -> {
    ResultSet resultSet = connection.prepareStatement("select * from users where id = 10").executeQuery();
    // consume the resultSet and return some value
def someDatabaseOperation(): Unit = {
  database.withTransaction(TransactionIsolationLevel.ReadUncommitted) { connection =>
    val resultSet: ResultSet = connection.prepareStatement("select * from users where id = 10").executeQuery();
    // consume the resultSet and return some value

The available transaction isolation levels mimic what is defined in java.sql.Connection.

Next: Migration Guides