Fork me on GitHub

Javalin 3 to 4 migration guide

This page attempts to cover all the things you need to know in order to migrate from Javalin 3 to Javalin 4. If you find any errors, please edit this page on GitHub.

Routing and path parameters

The path parameter syntax has changed from :param to {param}. A new <param> syntax has also been introduced, which allows for path parameters to accept slashes. A single path segment can now include both static and dynamic parts, and multiple path params:

get("/:param", ...) // old
get("/{param}", ...) // new

get("/:param-suffix", ...) // invalid
get("/{param}-suffix", ...) // new

get("/files/:filename.:extension", ...) // invalid
get("/files/{filename}.{extension}", ...) // new

get("/root/:subpaths/leaf") // old (will only match /root/1/leaf)
get("/root/<subpaths>/leaf") // new (will match /root/1/2/3/leaf)

The ctx.splat(index) method has been removed in favor of the new ctx.pathParam(key) method, since this method now supports multiple subpaths.

Static files configuration

Static file handling in Javalin 3 was an adventure. You could declare static file handlers like:

config.addStaticFiles(path, location);
config.addStaticFiles(path, location, hostedPath);

You could also configure some global options:

config.precompressStaticFiles = true/false;
config.aliasCheckForStaticFiles = ContextHandler.AliasCheck;

And in addition, there was special handling if you put files in a folder called immutable.

In Javalin 4, you can add handlers like this:

config.addStaticFiles(path, location); // location is now required, default is gone

The StaticFileConfig class has the following options:

var hostedPath: String = "/",
var directory: String = "/public",
var location: Location = Location.CLASSPATH,
var precompress: Boolean = false,
var aliasCheck: AliasCheck? = null,
var headers: Map<String, String> = mapOf(Header.CACHE_CONTROL to "max-age=0")
var skipFileFunction: (HttpServletRequest) -> Boolean = { false },

The global options have been removed, so all options are now per static file handler. These options can be configure in a familiar Javalin way:

config.addStaticFiles(staticFiles -> {
    staticFiles.aliasCheck = ContextHandler.AliasCheck((path, resource) -> !path.endsWith(".txt")); = "src/test/external/";
    staticFiles.location = Location.EXTERNAL;


Routes used to accept a Set<Role>, but they now accept a RouteRole... (varargs):

get("/path", controller::method, roles(MyRole.ROLENAME)) // old, using util-method
get("/path", controller::method, new HashSet<>(Arrays.asList(MyRole.ROLENAME));) // old, no util method
get("/path", controller::method, Role.ROLENAME) // new

The marker interface was renamed from Role to RouteRole, both because it’s clearer in the internal code what a RouteRole is, and because it’s tempting for end-users to name their role enum Role.

Validation rework

The check method can now take a map of arguments, which is useful for localization:

    .check({ it >= 18 }, ValidationError("AGE_TOO_LOW", args = mapOf("minAge" to 18)))

Validation errors are now provided as a map:

{"age": [{"message":"NULLCHECK_FAILED", "args":{}, "value": null}]} // POST /age-check
{"age": [{"message":"AGE_TOO_LOW", "args": {"minAge": 18}, "value": 16}]} // POST /age-check?age=16

How validation errors are handled can be changed by overriding the ValidationException:

app.exception( { e, ctx ->
    e.errors // do stuff with errors

Removed default overloads for queryParam and formParam

The ctx.queryParam("key", "default") and ctx.formParam("key", "default") methods have been removed. You can use Validator#getOrDefault(), or rely on the built in mechanisms for defaults in your language. This would mean the ?: operator in Kotlin, or Optional#ofNullable in Java.

Reified methods on Context

The reified methods on Context and WsContext have been renamed from ctx.queryParam<Int> to ctx.queryParamAsClass<Int>("key"). This change was necessary because mocking libraries were confused by the seemingly identical signatures of queryParam(key): String and reified queryParam(key): T

JavalinJson moves to config.jsonMapper(jsonMapper)

The JavalinJson has been replaced by a JsonMapper interface, which has four optional methods:

default String toJsonString(@NotNull Object obj) { // basic method for mapping to json
default InputStream toJsonStream(@NotNull Object obj) { // memory efficient method for mapping to json
default <T> T fromJsonString(@NotNull String json, @NotNull Class<T> targetClass) { // basic method for mapping from json
default <T> T fromJsonStream(@NotNull InputStream json, @NotNull Class<T> targetClass) { // memory efficient method for mapping from json

You only have to implement the methods you will be using. If you implement fromJsonStream, Javalin will pick this automatically over fromJsonString when deserializing a request body to an object, but toJsonStream needs to be called explicitly by doing ctx.json(obj, useStreamingMapper = true). This is because it uses an extra thread, and will only be beneficial for bigger JSON objects (unless you are using Project Loom).

To configure Javalin to use your JsonMapper, do config.jsonMapper(myJsonMapper).

Future rework

In Javalin 3, we had ctx.json(future) and ctx.result(future). In Javalin 4 we have ctx.future(future), and ctx.future(future, callback). The default callback is implemented as:

when (result) {
    is InputStream -> result(result)
    is String -> result(result)
    is Any -> json(result)

Meaning it will be set as a result if it’s an InputStream or a String, and it will be transformed into JSON if it’s any other type of object. If you need custom handling, you can now define this easily:

ctx.future(myFuture) { result ->
    if (result != null) {
    } else {

This should provide a lot more flexibility when working with futures in Javalin.

maxRequestSize replaces requestCacheSize

The old requestCacheSize has been removed, and maxRequestSize is now set to 1mb by default. This means that requests up to 1mb will be cached when you call ctx.body(). Requests larger than 1mb require you to use ctx.bodyAsInputStream() in order to read them (unless you adjust maxRequestSize).

compressionStrategy replaces dynamicGzip

The old dynamicGzip = true/false has been replaced by config.compressionStrategy(Brotli(level), Gzip(level)). To disable compression, do config.compressionStrategy(CompressionStrategy.NONE).

A RateLimiter reality check

The RateLimiter in Javalin is very naive, and in Javalin 4 we decided to be a bit more aggressive in letting people know this. The RateLimiter is now called NaiveRateLimiter.

Multiple calls to ctx.cookie(name) will now only result in one cookie being set. In Javalin 3 this method functioned as addCookie, but in Javalin 4 this has been fixed so it behaves like setCookie.

Internal config is now _conf

The internal config is public for advanced users, but we noticed a lot of beginners using it accidentally. It’s been renamed to _conf to discourage (mis)use.

WebSocket renaming

App attributes signatures

The Javalin#attribute(class, value) method is now Javalin#attribute(string, value).

Default port 8080

The default port was changed from 7000 to 8080 to conform with other JVM frameworks.

Context extensions are gone

The Context#register and Context#use methods have been removed.

These methods were intended for Java users who couldn’t use Kotlin extension functions. You can instead create a class which wraps context:

void myHandler(Context ctx) {
    ContextExtensions myExtensions = new ContextExtensions(ctx);

Like Javalin?
Star us 😊