Repository: tipsy/javalin
Pull request #1295 (2021-06-27 18:23:12)

[json] Remove JavalinJson object
This change gets rid of:

```kotlin
@FunctionalInterface
interface FromJsonMapper {
    fun <T> map(json: String, targetClass: Class<T>): T
}

@FunctionalInterface
interface ToJsonMapper {
    fun map(obj: Any): String
}

object JavalinJson {

    @JvmStatic
    var fromJsonMapper = JavalinJackson.defaultFromMapper

    @JvmStatic
    var toJsonMapper = JavalinJackson.defaultToMapper

    @JvmStatic
    fun toJson(obj: Any) = toJsonMapper.map(obj)

    @JvmStatic
    fun <T> fromJson(json: String, targetClass: Class<T>) = fromJsonMapper.map(json, targetClass)

}
```

And replaces it with a Java interface  (since Kotlin doesn't support default methods)

```java
// everything is annotated with @NotNull, but I removed it from the snippet here so it's easier to read
public interface JsonMapper {
    default <T> T fromJson(String json, Class<T> targetClass) {
        throw new NotImplementedError("JsonMapper#fromJson not implemented");
    }
    default String toJson(Object obj) {
        throw new NotImplementedError("JsonMapper#toJson not implemented");
    }
}
```

```java
public void jsonMapper(JsonMapper jsonMapper) {
    inner.appAttributes.put(JSON_MAPPER_KEY, jsonMapper);
}
```

Which is lives as a normal config option:

```kotlin
 val app = Javalin.create { it.jsonMapper(myJsonMapper) }
```

If no mapper is configured, Javalin adds a Jackson mapper (which logs if Jackson is not available, just like in Javalin 3)

tipsy: @rbygrave I belive you mentioned doing something in your fork? Any thoughts on this change?
rbygrave: > @rbygrave I belive you mentioned doing something in your fork? Any thoughts on this change?

IMO it looks good. In Jex Context is an interface, there is also an an spi interface JsonService.

The "guts" is that there is a ServiceManager which provides, JsonService, TemplateManager, ErrorHandler and MultipartUtil functionality to the context implementation. ServiceManager is basically a proxy to those services.

ServiceManager:
https://github.com/avaje/avaje-jex/blob/master/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java

The context implementation - JexHttpContext takes the service manager as constructor arg.
https://github.com/avaje/avaje-jex/blob/master/avaje-jex/src/main/java/io/avaje/jex/jetty/JexHttpContext.java#L39

https://github.com/avaje/avaje-jex/blob/master/avaje-jex/src/main/java/io/avaje/jex/spi/JsonService.java
https://github.com/avaje/avaje-jex/blob/master/avaje-jex/src/main/java/io/avaje/jex/core/JacksonJsonService.java

For Jex the JSON marshalling/unmarshalling uses the inputStream and outputStream directly at the moment but I think it should be byte[] based apart from jsonWriteStream() which should stay using the outputstream/generator.
 
rbygrave: > @rbygrave I belive you mentioned doing something in your fork? Any thoughts on this change?

Oh also https://github.com/tipsy/javalin/issues/1285 ... is probably more what you meant. I believe an option would be to add to the `JsonMapper` interface adding something like:

```java
  /**
   * Write the beans as {@literal x-json-stream } JSON with new line delimiter.
   */
  <E> void jsonWriteStream(Iterator<E> stream, SpiContext ctx);
```
The Jackson implementation of that in Jex is: https://github.com/avaje/avaje-jex/blob/master/avaje-jex/src/main/java/io/avaje/jex/core/JacksonJsonService.java#L50

tipsy: @rbygrave sorry I wasn't specific, I was actually referring to what you commented on with your first comment, but your second comment also comes at at good time. We can't add that function to the interface without providing default implementations, so I will have a look at that now.
tipsy: Changed the interface to Java now, so it can support default methods properly. Default implementation just throws.
tipsy: @rbygrave I'm not familiar with this approach, so please bear with me. Your signature is

```java
<E> void jsonWriteStream(Iterator<E> stream, SpiContext ctx);
```

But purely from an end-user perspective I think I would prefer to see:

```java
default void toJson(Object obj, OutputStream outputStream)
```

Any reason why you didn't do it like this?
    

RalphSteinhagen: Do you need the default implementation (ie. throwing an exception). Providing too many 'default' implementations (actually, they all are)  hint that one should use an abstract class rather than an interface... or did I miss something obvious from accessing these from a Kotlin point-of-view?

If you want to force that the user need to implement the interface one should write:
```Java
public interface JsonMapper {
    <T> T fromJson(@NotNull final String json, @NotNull final Class<T> targetClass);
    <T> String toJson(@NotNull final T obj);
}
```
The second `<T>` as well as the '@NotNull` and `final` would express more the intend/contract for this interface (ie. "please don't insert null pointers & you may serialise any class/reflection is involved,... "). These are not mandatory from a Java language standard point of view, but these would be picked-up by smart IDEs that auto-generate/check the user-implemented impl. prototype.

One of the advantages is that by not defining a `default` you get the UB (& null-pointer checks etc.) already during compile-time rather than only as late as an (possibly) unchecked exception during run-time.... :thinking: 
tipsy: > Do you need the default implementation (ie. throwing an exception).

I'm not a big fan of forcing the implementation of methods that will go unused, so IMO all of the methods should be optional/default. There are apps that don't accept user input, in which case requiring a `fromJson` would be unreasonable. Most apps won't use `application/x-json-stream`, so it would be very unreasonable to always require an implementation of that method.

> Providing too many 'default' implementations (actually, they all are) hint that one should use an abstract class rather than an interface... or did I miss something obvious from accessing these from a Kotlin point-of-view?

I don't share this perspective. If you can choose between an interface and an abstract class, you should always choose an interface, as it makes it much clearer what you're getting (all public, no state).

> The second <T> as well as the '@NotNull`

I'm not sure what that T adds for someone implementing this? The methods are both annotated with `@NotNull` on the current branch, I just removed the annotations from the PR description because I thought it was noisy. I will add a small comment.

tipsy: I'll merge this now, we'll handle extending the interface with streaming the response separately.

Patch:
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
index db2e7357..f510e724 100644
--- a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
@@ -5,7 +5,6 @@ import com.expediagroup.graphql.toSchema
 import graphql.GraphQL
 import io.javalin.http.Context
 import io.javalin.plugin.graphql.graphql.GraphQLRun
-import io.javalin.plugin.json.JavalinJson
 import io.javalin.websocket.WsMessageContext
 
 class GraphQLHandler(val options: GraphQLOptions) {
@@ -14,10 +13,10 @@ class GraphQLHandler(val options: GraphQLOptions) {
     init {
         val config = SchemaGeneratorConfig(supportedPackages = options.packages)
         val schema = toSchema(
-                config = config,
-                queries = options.queries,
-                mutations = options.mutations,
-                subscriptions = options.subscriptions
+            config = config,
+            queries = options.queries,
+            mutations = options.mutations,
+            subscriptions = options.subscriptions
         )
         this.graphQL = GraphQL.newGraphQL(schema).build()!!
     }
@@ -25,17 +24,18 @@ class GraphQLHandler(val options: GraphQLOptions) {
     fun execute(ctx: Context) {
         val body = ctx.bodyAsClass(Map::class.java)
         this.options.middleHandler(ctx)
-        val result = this.genericExecute(body)
-                .execute()
-                .thenApplyAsync { JavalinJson.toJson(it) }
-        ctx.contentType("application/json").future(result)
+        ctx.future(this.genericExecute(body).execute()) { result ->
+            if (result != null) {
+                ctx.json(result)
+            }
+        }
     }
 
     fun execute(ctx: WsMessageContext) {
         val body = ctx.message(Map::class.java)
         this.options.wsMiddleHandler(ctx)
         this.genericExecute(body)
-                .subscribe(SubscriberGraphQL(ctx))
+            .subscribe(SubscriberGraphQL(ctx))
     }
 
 
@@ -45,10 +45,10 @@ class GraphQLHandler(val options: GraphQLOptions) {
         val operationName = body.get("operationName")?.toString()
 
         return GraphQLRun(graphQL)
-                .withQuery(query)
-                .withVariables(variables)
-                .withOperationName(operationName)
-                .withContext(options.context);
+            .withQuery(query)
+            .withVariables(variables)
+            .withOperationName(operationName)
+            .withContext(options.context);
     }
 
 
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/SubscriberGraphQL.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/SubscriberGraphQL.kt
index fc9bc1e7..32831984 100644
--- a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/SubscriberGraphQL.kt
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/SubscriberGraphQL.kt
@@ -1,14 +1,13 @@
 package io.javalin.plugin.graphql
 
 import graphql.ExecutionResult
-import io.javalin.plugin.json.JavalinJson
 import io.javalin.websocket.WsMessageContext
 import org.reactivestreams.Subscriber
 import org.reactivestreams.Subscription
 import java.io.IOException
 import java.util.concurrent.atomic.AtomicReference
 
-class SubscriberGraphQL(private val ctx: WsMessageContext) : Subscriber<ExecutionResult> {
+class SubscriberGraphQL(private val wsContext: WsMessageContext) : Subscriber<ExecutionResult> {
     private val subscriptionRef = AtomicReference<Subscription>()
 
     override fun onError(error: Throwable) {
@@ -26,7 +25,7 @@ class SubscriberGraphQL(private val ctx: WsMessageContext) : Subscriber<Executio
     override fun onNext(result: ExecutionResult) {
         try {
             val data = result.getData<Any>()
-            ctx.send(JavalinJson.toJson(data))
+            wsContext.send(data)
         } catch (e: IOException) {
             e.printStackTrace()
         }
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/graphql/GraphQLRun.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/graphql/GraphQLRun.kt
index 0e987aa0..df708d83 100644
--- a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/graphql/GraphQLRun.kt
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/graphql/GraphQLRun.kt
@@ -4,7 +4,6 @@ import graphql.ExceptionWhileDataFetching
 import graphql.ExecutionInput
 import graphql.ExecutionResult
 import graphql.GraphQL
-import org.eclipse.jetty.server.handler.ContextHandler
 import org.reactivestreams.Publisher
 import org.reactivestreams.Subscriber
 import java.util.concurrent.CompletableFuture
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt
index e7bec01f..f12bb8da 100644
--- a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt
@@ -6,12 +6,12 @@ import io.javalin.core.security.RouteRole
 import io.javalin.http.Context
 import io.javalin.http.Handler
 import io.javalin.http.HandlerType
-import io.javalin.plugin.json.ToJsonMapper
 import io.javalin.plugin.openapi.annotations.HttpMethod
 import io.javalin.plugin.openapi.dsl.OpenApiDocumentation
 import io.javalin.plugin.openapi.dsl.documented
 import io.javalin.plugin.openapi.jackson.JacksonModelConverterFactory
 import io.javalin.plugin.openapi.jackson.JacksonToJsonMapper
+import io.javalin.plugin.openapi.jackson.ToJsonMapper
 import io.javalin.plugin.openapi.ui.ReDocOptions
 import io.javalin.plugin.openapi.ui.SwaggerOptions
 import io.javalin.plugin.openapi.utils.LazyDefaultValue
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/extractDocumentation.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/extractDocumentation.kt
index 086a52bc..735bff58 100644
--- a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/extractDocumentation.kt
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/extractDocumentation.kt
@@ -9,7 +9,13 @@ import io.javalin.core.util.methodReferenceReflectionMethodName
 import io.javalin.http.Handler
 import io.javalin.http.HandlerType
 import io.javalin.plugin.openapi.CreateSchemaOptions
-import io.javalin.plugin.openapi.annotations.*
+import io.javalin.plugin.openapi.annotations.HttpMethod
+import io.javalin.plugin.openapi.annotations.NULL_STRING
+import io.javalin.plugin.openapi.annotations.OpenApi
+import io.javalin.plugin.openapi.annotations.asOpenApiDocumentation
+import io.javalin.plugin.openapi.annotations.pathInfo
+import io.javalin.plugin.openapi.annotations.scanForAnnotations
+import io.javalin.plugin.openapi.annotations.warnUserAboutPotentialBugs
 import org.slf4j.LoggerFactory
 import java.lang.reflect.Field
 import java.lang.reflect.Method
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JacksonToJsonMapper.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JacksonToJsonMapper.kt
index 73302117..af9e0b84 100644
--- a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JacksonToJsonMapper.kt
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JacksonToJsonMapper.kt
@@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.databind.module.SimpleModule
 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import io.javalin.plugin.json.ToJsonMapper
 import io.javalin.plugin.openapi.utils.LazyDefaultValue
 import io.swagger.v3.core.jackson.mixin.SchemaMixin
 import io.swagger.v3.oas.models.media.Schema
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JavalinModelResolver.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JavalinModelResolver.kt
index 12d4976d..27610090 100644
--- a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JavalinModelResolver.kt
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JavalinModelResolver.kt
@@ -8,7 +8,6 @@ import io.swagger.v3.core.converter.ModelConverter
 import io.swagger.v3.core.converter.ModelConverterContext
 import io.swagger.v3.core.jackson.ModelResolver
 import io.swagger.v3.core.util.PrimitiveType
-import io.swagger.v3.oas.models.media.DateTimeSchema
 import io.swagger.v3.oas.models.media.Schema
 import java.time.Instant
 
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/ToJsonMapper.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/ToJsonMapper.kt
new file mode 100644
index 00000000..04571995
--- /dev/null
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/ToJsonMapper.kt
@@ -0,0 +1,6 @@
+package io.javalin.plugin.openapi.jackson
+
+@FunctionalInterface
+interface ToJsonMapper {
+    fun map(obj: Any): String
+}
diff --git a/javalin/src/main/java/io/javalin/core/JavalinConfig.java b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
index 9b1d5b7e..1db56b35 100644
--- a/javalin/src/main/java/io/javalin/core/JavalinConfig.java
+++ b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
@@ -29,6 +29,8 @@ import io.javalin.http.staticfiles.JettyResourceHandler;
 import io.javalin.http.staticfiles.Location;
 import io.javalin.http.staticfiles.ResourceHandler;
 import io.javalin.http.staticfiles.StaticFileConfig;
+import io.javalin.plugin.json.JavalinJackson;
+import io.javalin.plugin.json.JsonMapper;
 import io.javalin.websocket.WsConfig;
 import java.util.HashMap;
 import java.util.Map;
@@ -43,6 +45,7 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import static io.javalin.http.util.ContextUtil.maxRequestSizeKey;
+import static io.javalin.plugin.json.JsonMapperKt.JSON_MAPPER_KEY;
 
 public class JavalinConfig {
     // @formatter:off
@@ -54,7 +57,7 @@ public class JavalinConfig {
     public boolean ignoreTrailingSlashes = true;
     @NotNull public String defaultContentType = "text/plain";
     @NotNull public String contextPath = "/";
-    public Long maxRequestSize = 1_000_000L; // server will not accept payloads larger than 1mb by default
+    public Long maxRequestSize = 1_000_000L; // either increase this or use inputstream to handle large requests
     @NotNull public Long asyncRequestTimeout = 0L;
     @NotNull public Inner inner = new Inner();
 
@@ -189,6 +192,10 @@ public class JavalinConfig {
         registerPlugin(new HeadersPlugin(headers.get()));
     }
 
+    public void jsonMapper(JsonMapper jsonMapper) {
+        inner.appAttributes.put(JSON_MAPPER_KEY, jsonMapper);
+    }
+
     public static void applyUserConfig(Javalin app, JavalinConfig config, Consumer<JavalinConfig> userConfig) {
         userConfig.accept(config); // apply user config to the default config
 
@@ -211,6 +218,7 @@ public class JavalinConfig {
         if (config.enforceSsl) {
             app.before(SecurityUtil::sslRedirect);
         }
+        config.inner.appAttributes.putIfAbsent(JSON_MAPPER_KEY, new JavalinJackson());
         app.attribute(maxRequestSizeKey, config.maxRequestSize);
     }
 
diff --git a/javalin/src/main/java/io/javalin/core/event/EventManager.kt b/javalin/src/main/java/io/javalin/core/event/EventManager.kt
index e6fd8179..ac577329 100644
--- a/javalin/src/main/java/io/javalin/core/event/EventManager.kt
+++ b/javalin/src/main/java/io/javalin/core/event/EventManager.kt
@@ -9,7 +9,6 @@ package io.javalin.core.event
 import io.javalin.core.security.RouteRole
 import io.javalin.http.HandlerType
 import io.javalin.websocket.WsHandlerType
-import java.util.*
 import java.util.function.Consumer
 
 class EventManager {
diff --git a/javalin/src/main/java/io/javalin/core/util/Headers.kt b/javalin/src/main/java/io/javalin/core/util/Headers.kt
index 1cfd65a9..143cb1e7 100644
--- a/javalin/src/main/java/io/javalin/core/util/Headers.kt
+++ b/javalin/src/main/java/io/javalin/core/util/Headers.kt
@@ -7,7 +7,7 @@
 package io.javalin.core.util
 
 import java.time.Duration
-import java.util.Locale
+import java.util.*
 
 /**
  * A plugin to configure arbitrary headers, with a focus on the OWASP secure headers project
diff --git a/javalin/src/main/java/io/javalin/core/util/Util.kt b/javalin/src/main/java/io/javalin/core/util/Util.kt
index 23a170f2..80b8576b 100644
--- a/javalin/src/main/java/io/javalin/core/util/Util.kt
+++ b/javalin/src/main/java/io/javalin/core/util/Util.kt
@@ -14,7 +14,6 @@ import java.io.File
 import java.net.URL
 import java.net.URLEncoder
 import java.util.*
-import java.util.ServiceLoader
 import java.util.zip.Adler32
 import java.util.zip.CheckedInputStream
 import javax.servlet.http.HttpServletResponse
diff --git a/javalin/src/main/java/io/javalin/core/validation/BaseValidator.kt b/javalin/src/main/java/io/javalin/core/validation/BaseValidator.kt
index 43ec7202..eb9f9692 100644
--- a/javalin/src/main/java/io/javalin/core/validation/BaseValidator.kt
+++ b/javalin/src/main/java/io/javalin/core/validation/BaseValidator.kt
@@ -7,7 +7,7 @@
 package io.javalin.core.validation
 
 import io.javalin.core.util.JavalinLogger
-import io.javalin.plugin.json.JavalinJson
+import io.javalin.plugin.json.JsonMapper
 
 typealias Check<T> = (T) -> Boolean
 
@@ -15,13 +15,13 @@ data class Rule<T>(val fieldName: String, val check: Check<T?>, val error: Valid
 data class ValidationError<T>(val message: String, val args: Map<String, Any?> = mapOf(), var value: Any? = null)
 class ValidationException(val errors: Map<String, List<ValidationError<Any>>>) : Exception()
 
-open class BaseValidator<T>(val stringValue: String?, val clazz: Class<T>, val fieldName: String) {
+open class BaseValidator<T>(val stringValue: String?, val clazz: Class<T>, val fieldName: String, jsonMapper: JsonMapper? = null) {
     private var typedValue: T? = null
     internal val rules = mutableListOf<Rule<T>>()
     private val errors by lazy {
         if (this is BodyValidator) {
             try {
-                typedValue = JavalinJson.fromJson(stringValue!!, clazz)
+                typedValue = jsonMapper!!.fromJson(stringValue!!, clazz)
             } catch (e: Exception) {
                 JavalinLogger.info("Couldn't deserialize body to ${clazz.simpleName}", e)
                 return@lazy mapOf(clazz.simpleName to listOf(ValidationError("DESERIALIZATION_FAILED", value = stringValue)))
diff --git a/javalin/src/main/java/io/javalin/core/validation/BodyValidator.kt b/javalin/src/main/java/io/javalin/core/validation/BodyValidator.kt
index 6e54a3af..4769fe9e 100644
--- a/javalin/src/main/java/io/javalin/core/validation/BodyValidator.kt
+++ b/javalin/src/main/java/io/javalin/core/validation/BodyValidator.kt
@@ -6,7 +6,9 @@
 
 package io.javalin.core.validation
 
-open class BodyValidator<T>(stringValue: String?, clazz: Class<T>) : BaseValidator<T>(stringValue, clazz, clazz.simpleName) {
+import io.javalin.plugin.json.JsonMapper
+
+open class BodyValidator<T>(stringValue: String?, clazz: Class<T>, jsonMapper: JsonMapper) : BaseValidator<T>(stringValue, clazz, clazz.simpleName, jsonMapper) {
     fun check(check: Check<T>, error: String) = check(clazz.simpleName, check, error)
     fun check(check: Check<T>, error: ValidationError<T>) = check(clazz.simpleName, check, error)
     fun check(fieldName: String, check: Check<T>, error: String) = addRule(fieldName, { check(it!!) }, error) as BodyValidator<T>
diff --git a/javalin/src/main/java/io/javalin/http/Context.kt b/javalin/src/main/java/io/javalin/http/Context.kt
index 19ea690c..19beeb63 100644
--- a/javalin/src/main/java/io/javalin/http/Context.kt
+++ b/javalin/src/main/java/io/javalin/http/Context.kt
@@ -15,7 +15,7 @@ import io.javalin.http.util.ContextUtil.throwPayloadTooLargeIfPayloadTooLarge
 import io.javalin.http.util.CookieStore
 import io.javalin.http.util.MultipartUtil
 import io.javalin.http.util.SeekableWriter
-import io.javalin.plugin.json.JavalinJson
+import io.javalin.plugin.json.jsonMapper
 import io.javalin.plugin.rendering.JavalinRenderer
 import java.io.InputStream
 import java.nio.charset.Charset
@@ -32,7 +32,7 @@ import javax.servlet.http.HttpServletResponse
  * @see <a href="https://javalin.io/documentation#context">Context in docs</a>
  */
 // don't suppress warnings, since annotated classes are ignored by dokka (yeah...)
-open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: HttpServletResponse, private val appAttributes: Map<String, Any> = mapOf()) {
+open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: HttpServletResponse, internal val appAttributes: Map<String, Any> = mapOf()) {
 
     // @formatter:off
     @get:JvmSynthetic @set:JvmSynthetic internal var inExceptionHandler = false
@@ -43,7 +43,7 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     @get:JvmSynthetic @set:JvmSynthetic internal var splatList = listOf<String>()
     // @formatter:on
 
-    private val cookieStore by lazy { CookieStore(cookie(CookieStore.COOKIE_NAME)) }
+    private val cookieStore by lazy { CookieStore(this.jsonMapper(), cookie(CookieStore.COOKIE_NAME)) }
     private val characterEncoding by lazy { ContextUtil.getRequestCharset(this) ?: "UTF-8" }
     private var resultStream: InputStream? = null
     private var resultFuture: CompletableFuture<*>? = null
@@ -121,11 +121,8 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
      */
     fun bodyAsInputStream(): InputStream = req.inputStream
 
-    /**
-     * Creates a [Validator] for the body() value, with the prefix "Request body as $clazz"
-     * Throws [BadRequestResponse] if validation fails.
-     */
-    fun <T> bodyValidator(clazz: Class<T>) = BodyValidator(body(), clazz)
+    /** Creates a [BodyValidator] for the body() value */
+    fun <T> bodyValidator(clazz: Class<T>) = BodyValidator(body(), clazz, this.jsonMapper())
 
     /** Reified version of [bodyValidator] */
     @JvmSynthetic
@@ -428,7 +425,7 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
      * JavalinJson can be configured to use any mapping library.
      */
     fun json(obj: Any): Context {
-        return contentType("application/json").result(JavalinJson.toJson(obj))
+        return contentType("application/json").result(this.jsonMapper().toJson(obj))
     }
 
     /**
diff --git a/javalin/src/main/java/io/javalin/http/ErrorMapper.kt b/javalin/src/main/java/io/javalin/http/ErrorMapper.kt
index 55bf5f39..16bb5fa8 100644
--- a/javalin/src/main/java/io/javalin/http/ErrorMapper.kt
+++ b/javalin/src/main/java/io/javalin/http/ErrorMapper.kt
@@ -7,7 +7,6 @@
 package io.javalin.http
 
 import io.javalin.core.util.Header
-import java.util.*
 
 class ErrorMapper {
     val errorHandlerMap = HashMap<Int, Handler>()
diff --git a/javalin/src/main/java/io/javalin/http/JavalinServlet.kt b/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
index 2fd7b987..316139d1 100644
--- a/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
+++ b/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
@@ -9,7 +9,6 @@ package io.javalin.http
 import io.javalin.core.JavalinConfig
 import io.javalin.core.security.RouteRole
 import io.javalin.core.util.CorsPlugin
-import io.javalin.core.util.Header
 import io.javalin.core.util.LogUtil
 import io.javalin.http.util.ContextUtil
 import io.javalin.http.util.MethodNotAllowedUtil
diff --git a/javalin/src/main/java/io/javalin/http/util/ContextUtil.kt b/javalin/src/main/java/io/javalin/http/util/ContextUtil.kt
index fb2318ce..37aa1df7 100644
--- a/javalin/src/main/java/io/javalin/http/util/ContextUtil.kt
+++ b/javalin/src/main/java/io/javalin/http/util/ContextUtil.kt
@@ -87,7 +87,7 @@ object ContextUtil {
         false
     }
 
-    fun changeBaseRequest(ctx: Context, req: HttpServletRequest) = Context(req, ctx.res).apply {
+    fun changeBaseRequest(ctx: Context, req: HttpServletRequest) = Context(req, ctx.res, ctx.appAttributes).apply {
         this.pathParamMap = ctx.pathParamMap
         this.matchedPath = ctx.matchedPath
     }
diff --git a/javalin/src/main/java/io/javalin/http/util/CookieStore.kt b/javalin/src/main/java/io/javalin/http/util/CookieStore.kt
index 62207436..52517cb0 100644
--- a/javalin/src/main/java/io/javalin/http/util/CookieStore.kt
+++ b/javalin/src/main/java/io/javalin/http/util/CookieStore.kt
@@ -6,12 +6,12 @@
 
 package io.javalin.http.util
 
-import io.javalin.plugin.json.JavalinJson
+import io.javalin.plugin.json.JsonMapper
 import java.util.*
 import javax.servlet.http.Cookie
 
 @Suppress("UNCHECKED_CAST")
-class CookieStore(cookie: String?) {
+class CookieStore(val jsonMapper: JsonMapper, cookie: String?) {
 
     companion object {
         var COOKIE_NAME = "javalin-cookie-store"
@@ -28,8 +28,8 @@ class CookieStore(cookie: String?) {
     fun clear() = cookieMap.clear()
 
     private fun deserialize(cookie: String?) = if (!cookie.isNullOrEmpty()) {
-        JavalinJson.fromJson(String(Base64.getDecoder().decode(cookie)), Map::class.java) as MutableMap<String, Any>
+        jsonMapper.fromJson(String(Base64.getDecoder().decode(cookie)), Map::class.java) as MutableMap<String, Any>
     } else mutableMapOf()
 
-    private fun serialize(map: MutableMap<String, Any>) = Base64.getEncoder().encodeToString(JavalinJson.toJson(map).toByteArray())
+    private fun serialize(map: MutableMap<String, Any>) = Base64.getEncoder().encodeToString(jsonMapper.toJson(map).toByteArray())
 }
diff --git a/javalin/src/main/java/io/javalin/plugin/json/JavalinJackson.kt b/javalin/src/main/java/io/javalin/plugin/json/JavalinJackson.kt
index 4d3c0615..2dda6536 100644
--- a/javalin/src/main/java/io/javalin/plugin/json/JavalinJackson.kt
+++ b/javalin/src/main/java/io/javalin/plugin/json/JavalinJackson.kt
@@ -11,51 +11,31 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import io.javalin.core.util.OptionalDependency
 import io.javalin.core.util.Util
 
-object JavalinJackson {
-
-    private var objectMapper: ObjectMapper? = null
-    private val defaultObjectMapper: ObjectMapper by lazy { defaultObjectMapper() }
-
-    @JvmStatic
-    fun configure(staticObjectMapper: ObjectMapper) {
-        objectMapper = staticObjectMapper
-    }
-
-    @JvmStatic
-    fun getObjectMapper(): ObjectMapper {
-        return (objectMapper ?: defaultObjectMapper)
-    }
-
-    @JvmStatic
-    fun defaultObjectMapper(): ObjectMapper = try {
+val defaultMapper by lazy {
+    try {
         val className = OptionalDependency.JACKSON_KT.testClass
         ObjectMapper().registerModule(Class.forName(className).getConstructor().newInstance() as Module)
     } catch (e: ClassNotFoundException) {
         ObjectMapper()
     }
+}
 
-    val defaultToMapper = object: ToJsonMapper {
-        override fun map(obj: Any): String = when (obj) {
-            is String -> obj // the default mapper treats strings as if they are already JSON
-            else -> toJson(obj) // convert object to JSON
-        }
-    }
+class JavalinJackson(val objectMapper: ObjectMapper = defaultMapper) : JsonMapper {
 
-    fun toJson(value: Any): String {
+    override fun toJson(obj: Any): String {
         Util.ensureDependencyPresent(OptionalDependency.JACKSON)
-        return (objectMapper ?: defaultObjectMapper).writeValueAsString(value)
-    }
-
-    val defaultFromMapper = object : FromJsonMapper {
-        override fun <T> map(json: String, targetClass: Class<T>): T = fromJson(json, targetClass)
+        return when (obj) {
+            is String -> obj // the default mapper treats strings as if they are already JSON
+            else -> objectMapper.writeValueAsString(obj) // convert object to JSON
+        }
     }
 
-    fun <T> fromJson(json: String, clazz: Class<T>): T {
+    override fun <T> fromJson(json: String, targetClass: Class<T>): T {
         Util.ensureDependencyPresent(OptionalDependency.JACKSON)
-        if (Util.isKotlinClass(clazz)) {
+        if (Util.isKotlinClass(targetClass)) {
             Util.ensureDependencyPresent(OptionalDependency.JACKSON_KT)
         }
-        return (objectMapper ?: defaultObjectMapper).readValue(json, clazz)
+        return objectMapper.readValue(json, targetClass)
     }
 
 }
diff --git a/javalin/src/main/java/io/javalin/plugin/json/JavalinJson.kt b/javalin/src/main/java/io/javalin/plugin/json/JavalinJson.kt
deleted file mode 100644
index 2c033f17..00000000
--- a/javalin/src/main/java/io/javalin/plugin/json/JavalinJson.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Javalin - https://javalin.io
- * Copyright 2017 David Åse
- * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
- */
-
-package io.javalin.plugin.json
-
-@FunctionalInterface
-interface FromJsonMapper {
-    fun <T> map(json: String, targetClass: Class<T>): T
-}
-
-@FunctionalInterface
-interface ToJsonMapper {
-    fun map(obj: Any): String
-}
-
-object JavalinJson {
-
-    @JvmStatic
-    var fromJsonMapper = JavalinJackson.defaultFromMapper
-
-    @JvmStatic
-    var toJsonMapper = JavalinJackson.defaultToMapper
-
-    @JvmStatic
-    fun toJson(obj: Any) = toJsonMapper.map(obj)
-
-    @JvmStatic
-    fun <T> fromJson(json: String, targetClass: Class<T>) = fromJsonMapper.map(json, targetClass)
-
-}
diff --git a/javalin/src/main/java/io/javalin/plugin/json/JsonMapper.java b/javalin/src/main/java/io/javalin/plugin/json/JsonMapper.java
new file mode 100644
index 00000000..6eae5ad7
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/plugin/json/JsonMapper.java
@@ -0,0 +1,13 @@
+package io.javalin.plugin.json;
+
+import kotlin.NotImplementedError;
+import org.jetbrains.annotations.NotNull;
+
+public interface JsonMapper {
+    default @NotNull <T> T fromJson(@NotNull String json, @NotNull Class<T> targetClass) {
+        throw new NotImplementedError("JsonMapper#fromJson not implemented");
+    }
+    default @NotNull String toJson(@NotNull Object obj) {
+        throw new NotImplementedError("JsonMapper#toJson not implemented");
+    }
+}
diff --git a/javalin/src/main/java/io/javalin/plugin/json/JsonMapper.kt b/javalin/src/main/java/io/javalin/plugin/json/JsonMapper.kt
new file mode 100644
index 00000000..a14f3263
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/plugin/json/JsonMapper.kt
@@ -0,0 +1,14 @@
+/*
+ * Javalin - https://javalin.io
+ * Copyright 2017 David Åse
+ * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+ */
+
+package io.javalin.plugin.json
+
+import io.javalin.Javalin
+import io.javalin.http.Context
+
+const val JSON_MAPPER_KEY = "global-json-mapper"
+fun Javalin.jsonMapper(): JsonMapper = this.attribute(JSON_MAPPER_KEY)
+fun Context.jsonMapper(): JsonMapper = this.appAttribute(JSON_MAPPER_KEY)
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueComponent.kt b/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueComponent.kt
index 11e8a715..f9da5e0c 100644
--- a/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueComponent.kt
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueComponent.kt
@@ -4,7 +4,7 @@ import io.javalin.core.util.Header
 import io.javalin.http.Context
 import io.javalin.http.Handler
 import io.javalin.http.InternalServerErrorResponse
-import io.javalin.plugin.json.JavalinJson
+import io.javalin.plugin.json.jsonMapper
 import io.javalin.plugin.rendering.vue.FileInliner.inlineFiles
 import io.javalin.plugin.rendering.vue.JavalinVue.cacheControl
 import io.javalin.plugin.rendering.vue.JavalinVue.cachedDependencyResolver
@@ -67,7 +67,7 @@ object FileInliner {
 
 internal fun getState(ctx: Context, state: Any?) = "\n<script>\n" +
         "${prototypeOrGlobalConfig()}.\$javalin = JSON.parse(decodeURIComponent(\"${
-            urlEncodeForJavascript(JavalinJson.toJson(
+            urlEncodeForJavascript(ctx.jsonMapper().toJson(
                     mapOf(
                             "pathParams" to ctx.pathParamMap(),
                             "queryParams" to ctx.queryParamMap(),
diff --git a/javalin/src/main/java/io/javalin/websocket/JavalinWsServlet.kt b/javalin/src/main/java/io/javalin/websocket/JavalinWsServlet.kt
index f7e862e4..1f9ee11b 100644
--- a/javalin/src/main/java/io/javalin/websocket/JavalinWsServlet.kt
+++ b/javalin/src/main/java/io/javalin/websocket/JavalinWsServlet.kt
@@ -28,7 +28,7 @@ internal const val upgradeSessionAttrsKey = "javalin-ws-upgrade-http-session"
 /**
  * The JavalinWsServlet is responsible for both WebSocket and HTTP requests.
  * It extends Jetty's WebSocketServlet, and has a HTTP Servlet as a constructor arg.
- * It switches between WebSocket and HTTP in the `service` method.
+ * It switches between WebSocket and HTTP in the [service] method.
  */
 class JavalinWsServlet(val config: JavalinConfig, private val httpServlet: JavalinServlet) : WebSocketServlet() {
 
@@ -53,14 +53,13 @@ class JavalinWsServlet(val config: JavalinConfig, private val httpServlet: Javal
         val requestUri = req.requestURI.removePrefix(req.contextPath)
         val entry = wsPathMatcher.findEndpointHandlerEntry(requestUri) ?: return res.sendError(404, "WebSocket handler not found")
         try {
-            val upgradeContext = Context(req, res).apply {
+            val upgradeContext = Context(req, res, config.inner.appAttributes).apply {
                 pathParamMap = entry.extractPathParams(requestUri)
                 matchedPath = entry.path
             }
-            config.inner.accessManager.manage({ ctx -> ctx.req.setAttribute(upgradeAllowedKey, true) }, upgradeContext, entry.roles)
+            config.inner.accessManager.manage({ it.req.setAttribute(upgradeAllowedKey, true) }, upgradeContext, entry.roles)
             if (req.getAttribute(upgradeAllowedKey) != true) throw UnauthorizedResponse() // if set to true, the access manager ran the handler (== valid)
             req.setAttribute(upgradeContextKey, upgradeContext)
-
             val wsProtocolHeader = req.getHeader(WebSocketConstants.SEC_WEBSOCKET_PROTOCOL)
             if (wsProtocolHeader != null) {
                 val protocolNames = wsProtocolHeader.split(',').map { it.trim() }.filter { it != "" }
@@ -68,7 +67,6 @@ class JavalinWsServlet(val config: JavalinConfig, private val httpServlet: Javal
                     res.setHeader(WebSocketConstants.SEC_WEBSOCKET_PROTOCOL, protocolNames.first())
                 }
             }
-
             super.service(req, res) // everything is okay, perform websocket upgrade
         } catch (e: Exception) {
             res.sendError(401, "Unauthorized")
diff --git a/javalin/src/main/java/io/javalin/websocket/WsContext.kt b/javalin/src/main/java/io/javalin/websocket/WsContext.kt
index ceffb246..0151edff 100644
--- a/javalin/src/main/java/io/javalin/websocket/WsContext.kt
+++ b/javalin/src/main/java/io/javalin/websocket/WsContext.kt
@@ -7,7 +7,7 @@
 package io.javalin.websocket
 
 import io.javalin.http.Context
-import io.javalin.plugin.json.JavalinJson
+import io.javalin.plugin.json.jsonMapper
 import org.eclipse.jetty.websocket.api.RemoteEndpoint
 import org.eclipse.jetty.websocket.api.Session
 import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest
@@ -20,13 +20,13 @@ import java.nio.ByteBuffer
  */
 abstract class WsContext(val sessionId: String, @JvmField val session: Session) {
 
-    private val upgradeReq by lazy { session.upgradeRequest as ServletUpgradeRequest }
-    private val upgradeCtx by lazy { upgradeReq.httpServletRequest.getAttribute(upgradeContextKey) as Context }
-    private val sessionAttributes by lazy { upgradeReq.httpServletRequest.getAttribute(upgradeSessionAttrsKey) as Map<String, Any>? }
+    internal val upgradeReq by lazy { session.upgradeRequest as ServletUpgradeRequest }
+    internal val upgradeCtx by lazy { upgradeReq.httpServletRequest.getAttribute(upgradeContextKey) as Context }
+    internal val sessionAttributes by lazy { upgradeReq.httpServletRequest.getAttribute(upgradeSessionAttrsKey) as Map<String, Any>? }
 
     fun matchedPath() = upgradeCtx.matchedPath
 
-    fun send(message: Any) = send(JavalinJson.toJson(message))
+    fun send(message: Any) = send(upgradeCtx.jsonMapper().toJson(message))
     fun send(message: String) = session.remote.sendStringByFuture(message)
     fun send(message: ByteBuffer) = session.remote.sendBytesByFuture(message)
 
@@ -78,5 +78,5 @@ class WsBinaryMessageContext(sessionId: String, session: Session, private val da
 
 class WsMessageContext(sessionId: String, session: Session, private val message: String) : WsContext(sessionId, session) {
     fun message(): String = message
-    fun <T> message(clazz: Class<T>): T = JavalinJson.fromJson(message, clazz)
+    fun <T> message(clazz: Class<T>): T = upgradeCtx.jsonMapper().fromJson(message, clazz)
 }


Test patch:
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQL.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQL.kt
index 1cb3d0c3..181e723d 100644
--- a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQL.kt
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQL.kt
@@ -74,7 +74,6 @@ class TestGraphQL {
     @Test
     fun subscribe() = TestUtil.test(shortTimeoutServer()) { server, httpUtil ->
         val testClient1_1 = TestClient(server, graphqlPath)
-
         doAndSleepWhile({ testClient1_1.connect() }, { !testClient1_1.isOpen })
         doAndSleep { testClient1_1.send("{\"query\": \"subscription { counter }\"}") }
         doAndSleepWhile({ testClient1_1.close() }, { testClient1_1.isClosing })
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQLAdvanced.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQLAdvanced.kt
index 6afeb137..0dced74d 100644
--- a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQLAdvanced.kt
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQLAdvanced.kt
@@ -6,18 +6,10 @@ import io.javalin.plugin.graphql.helpers.ContextExample
 import io.javalin.plugin.graphql.helpers.MutationExample
 import io.javalin.plugin.graphql.helpers.QueryExample
 import io.javalin.plugin.graphql.helpers.SubscriptionExample
-import io.javalin.testing.HttpUtil
 import io.javalin.testing.TestUtil
 import junit.framework.Assert.assertEquals
-import org.assertj.core.api.Assertions.assertThat
-import org.java_websocket.client.WebSocketClient
-import org.java_websocket.drafts.Draft_6455
-import org.java_websocket.handshake.ServerHandshake
 import org.json.JSONObject
 import org.junit.Test
-import java.net.URI
-import java.time.Duration
-import java.util.concurrent.TimeoutException
 
 
 class TestGraphQLAdvanced {
diff --git a/javalin-openapi/src/test/java/io/javalin/examples/HelloWorldSwagger.java b/javalin-openapi/src/test/java/io/javalin/examples/HelloWorldSwagger.java
index 6c901475..a592eace 100644
--- a/javalin-openapi/src/test/java/io/javalin/examples/HelloWorldSwagger.java
+++ b/javalin-openapi/src/test/java/io/javalin/examples/HelloWorldSwagger.java
@@ -7,6 +7,7 @@
 package io.javalin.examples;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import io.javalin.Javalin;
 import io.javalin.http.Context;
 import io.javalin.plugin.json.JavalinJackson;
@@ -27,7 +28,7 @@ public class HelloWorldSwagger {
 
     public static void main(String[] args) {
 
-        JavalinJackson.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        ObjectMapper objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
 
         OpenApiOptions openApiOptions = new OpenApiOptions(new Info().version("1.0").description("My Application"))
             .activateAnnotationScanningFor("io.javalin.examples")
@@ -43,7 +44,10 @@ public class HelloWorldSwagger {
                 ).build()
             ).title("My ReDoc Documentation"));
 
-        Javalin app = Javalin.create(config -> config.registerPlugin(new OpenApiPlugin(openApiOptions))).start(7070);
+        Javalin app = Javalin.create(config -> {
+            config.registerPlugin(new OpenApiPlugin(openApiOptions));
+            config.jsonMapper(new JavalinJackson(objectMapper));
+        }).start(7070);
 
         app.post("/users", ExampleController::create);
 
diff --git a/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApi.kt b/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApi.kt
index 4d0a3175..add31fcf 100644
--- a/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApi.kt
+++ b/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApi.kt
@@ -44,7 +44,6 @@ import org.assertj.core.api.Assertions.assertThat
 import org.assertj.core.api.Assertions.assertThatExceptionOfType
 import org.junit.Test
 import java.time.Instant
-import java.util.*
 
 class Address(val street: String, val number: Int)
 
diff --git a/javalin-openapi/src/test/java/io/javalin/plugin/openapi/json.kt b/javalin-openapi/src/test/java/io/javalin/plugin/openapi/json.kt
index 0078c4db..ccadfccb 100644
--- a/javalin-openapi/src/test/java/io/javalin/plugin/openapi/json.kt
+++ b/javalin-openapi/src/test/java/io/javalin/plugin/openapi/json.kt
@@ -17,7 +17,7 @@ fun configureJacksonToJsonMapper() {
 
 fun String.formatJson(): String {
     configureJacksonToJsonMapper()
-    val node = JavalinJackson.fromJson(this, JsonNode::class.java)
+    val node = JavalinJackson().fromJson(this, JsonNode::class.java)
     return JacksonToJsonMapper().map(node)
 }
 
diff --git a/javalin-testtools/src/main/java/io/javalin/testtools/HttpClient.kt b/javalin-testtools/src/main/java/io/javalin/testtools/HttpClient.kt
index 403c7770..8fb08ff7 100644
--- a/javalin-testtools/src/main/java/io/javalin/testtools/HttpClient.kt
+++ b/javalin-testtools/src/main/java/io/javalin/testtools/HttpClient.kt
@@ -1,6 +1,7 @@
 package io.javalin.testtools
 
-import io.javalin.plugin.json.JavalinJson
+import io.javalin.Javalin
+import io.javalin.plugin.json.jsonMapper
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.OkHttpClient
 import okhttp3.Request
@@ -9,10 +10,10 @@ import okhttp3.RequestBody.Companion.toRequestBody
 import okhttp3.Response
 import java.util.function.Consumer
 
-class HttpClient(port: Int) {
+class HttpClient(val app: Javalin) {
 
     var okHttp = OkHttpClient()
-    var origin: String = "http://localhost:$port"
+    var origin: String = "http://localhost:${app.port()}"
 
     fun request(request: Request) = okHttp.newCall(request).execute()
     fun request(path: String, builder: Request.Builder) = request(builder.url(origin + path).build())
@@ -42,15 +43,16 @@ class HttpClient(port: Int) {
     fun delete(path: String, json: Any? = null, req: Consumer<Request.Builder>? = null) =
             request(path, combine(req, { it.delete(json.toRequestBody()) }))
 
+    private fun Any?.toRequestBody(): RequestBody {
+        return if (this == null) {
+            ByteArray(0).toRequestBody(null, 0, 0)
+        } else {
+            app.jsonMapper().toJson(this).toRequestBody(JSON_TYPE)
+        }
+    }
+
     companion object {
         private val JSON_TYPE = "application/json".toMediaType()
-        private fun Any?.toRequestBody(): RequestBody {
-            return if (this == null) {
-                ByteArray(0).toRequestBody(null, 0, 0)
-            } else {
-                JavalinJson.toJson(this).toRequestBody(JSON_TYPE)
-            }
-        }
     }
 
     private fun combine(userBuilder: Consumer<Request.Builder>?, utilBuilder: Consumer<Request.Builder>): Request.Builder {
diff --git a/javalin-testtools/src/main/java/io/javalin/testtools/TestUtil.kt b/javalin-testtools/src/main/java/io/javalin/testtools/TestUtil.kt
index 9d91c6d4..d22b4824 100644
--- a/javalin-testtools/src/main/java/io/javalin/testtools/TestUtil.kt
+++ b/javalin-testtools/src/main/java/io/javalin/testtools/TestUtil.kt
@@ -20,7 +20,7 @@ object TestUtil {
             JavalinLogger.enabled = false
         }
         app.start(0)
-        val http = HttpClient(app.port())
+        val http = HttpClient(app)
         test.accept(app, http) // this is where the user's test happens
         if (clearCookies) {
             val endpointUrl = "/clear-cookies-${UUID.randomUUID()}"
diff --git a/javalin/src/test/java/io/javalin/TestContextHandlerType.java b/javalin/src/test/java/io/javalin/TestContextHandlerType.java
index d871d27e..7476a558 100644
--- a/javalin/src/test/java/io/javalin/TestContextHandlerType.java
+++ b/javalin/src/test/java/io/javalin/TestContextHandlerType.java
@@ -7,11 +7,10 @@ package io.javalin;
 
 import io.javalin.http.HandlerType;
 import io.javalin.testing.TestUtil;
-import org.assertj.core.api.Assertions;
-import org.junit.Test;
-
 import java.util.ArrayList;
 import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
 
 public class TestContextHandlerType {
 
diff --git a/javalin/src/test/java/io/javalin/TestJavalinVue.kt b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
index 2c579339..568a6287 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinVue.kt
+++ b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
@@ -7,6 +7,8 @@
 package io.javalin
 
 import io.javalin.http.Context
+import io.javalin.plugin.json.JavalinJackson
+import io.javalin.plugin.json.jsonMapper
 import io.javalin.plugin.rendering.vue.JavalinVue
 import io.javalin.plugin.rendering.vue.VueComponent
 import io.javalin.testing.TestUtil
@@ -46,11 +48,12 @@ class TestJavalinVue {
     private val state = State(User("tipsy", "tipsy@tipsy.tipsy"), Role("Maintainer"))
 
     private fun String.uriEncodeForJavascript() =
-            URLEncoder.encode(this, Charsets.UTF_8.name()).replace("+", "%20")
+        URLEncoder.encode(this, Charsets.UTF_8.name()).replace("+", "%20")
 
     @Test
     fun `vue component with state`() = TestUtil.test { app, http ->
-        val encodedState = """{"pathParams":{"my-param":"test-path-param"},"queryParams":{"qp":["test-query-param"]},"state":{"user":{"name":"tipsy","email":"tipsy@tipsy.tipsy"},"role":{"name":"Maintainer"}}}""".uriEncodeForJavascript()
+        val encodedState =
+            """{"pathParams":{"my-param":"test-path-param"},"queryParams":{"qp":["test-query-param"]},"state":{"user":{"name":"tipsy","email":"tipsy@tipsy.tipsy"},"role":{"name":"Maintainer"}}}""".uriEncodeForJavascript()
         JavalinVue.stateFunction = { ctx -> state }
         app.get("/vue/:my-param", VueComponent("<test-component></test-component>"))
         val res = http.getBody("/vue/test-path-param?qp=test-query-param")
@@ -87,7 +90,8 @@ class TestJavalinVue {
     @Test
     fun `vue3 component with state`() = TestUtil.test { app, http ->
         JavalinVue.vueVersion { it.vue3("app") }
-        val encodedState = """{"pathParams":{"my-param":"test-path-param"},"queryParams":{"qp":["test-query-param"]},"state":{"user":{"name":"tipsy","email":"tipsy@tipsy.tipsy"},"role":{"name":"Maintainer"}}}""".uriEncodeForJavascript()
+        val encodedState =
+            """{"pathParams":{"my-param":"test-path-param"},"queryParams":{"qp":["test-query-param"]},"state":{"user":{"name":"tipsy","email":"tipsy@tipsy.tipsy"},"role":{"name":"Maintainer"}}}""".uriEncodeForJavascript()
         JavalinVue.stateFunction = { ctx -> state }
         app.get("/vue/:my-param", VueComponent("<test-component-3></test-component-3>"))
         val res = http.getBody("/vue/test-path-param?qp=test-query-param")
@@ -187,6 +191,7 @@ class TestJavalinVue {
     @Test
     fun `@cdnWebjar resolves to webjar on localhost`() = TestUtil.test { app, http ->
         val ctx = mockk<Context>(relaxed = true)
+        every { ctx.jsonMapper() } returns JavalinJackson()
         JavalinVue.isDev = true // reset
         every { ctx.url() } returns "http://localhost:1234/"
         VueComponent("<test-component></test-component>").handle(ctx)
@@ -197,6 +202,7 @@ class TestJavalinVue {
     @Test
     fun `@cdnWebjar resolves to cdn on non-localhost`() = TestUtil.test { app, http ->
         val ctx = mockk<Context>(relaxed = true)
+       every { ctx.jsonMapper() } returns JavalinJackson()
         every { ctx.url() } returns "https://example.com"
         VueComponent("<test-component></test-component>").handle(ctx)
         val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
@@ -206,6 +212,7 @@ class TestJavalinVue {
     @Test
     fun `@cdnWebjar resolves to https even on non https hosts`() = TestUtil.test { app, http ->
         val ctx = mockk<Context>(relaxed = true)
+       every { ctx.jsonMapper() } returns JavalinJackson()
         every { ctx.url() } returns "http://123.123.123.123:1234/"
         VueComponent("<test-component></test-component>").handle(ctx)
         val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
@@ -215,6 +222,7 @@ class TestJavalinVue {
     @Test
     fun `@inlineFile functionality works as expected if not-dev`() = TestUtil.test { app, http ->
         val ctx = mockk<Context>(relaxed = true)
+       every { ctx.jsonMapper() } returns JavalinJackson()
         every { ctx.url() } returns "http://123.123.123.123:1234/"
         VueComponent("<test-component></test-component>").handle(ctx)
         val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
@@ -228,6 +236,7 @@ class TestJavalinVue {
     @Test
     fun `@inlineFile functionality works as expected if dev`() = TestUtil.test { app, http ->
         val ctx = mockk<Context>(relaxed = true)
+       every { ctx.jsonMapper() } returns JavalinJackson()
         every { ctx.url() } returns "http://localhost:1234/"
         VueComponent("<test-component></test-component>").handle(ctx)
         val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
diff --git a/javalin/src/test/java/io/javalin/TestJson.kt b/javalin/src/test/java/io/javalin/TestJson.kt
index ec18bd4e..86216f56 100644
--- a/javalin/src/test/java/io/javalin/TestJson.kt
+++ b/javalin/src/test/java/io/javalin/TestJson.kt
@@ -7,12 +7,10 @@
 package io.javalin
 
 import com.google.gson.GsonBuilder
-import com.mashape.unirest.http.Unirest
+import io.javalin.core.util.JavalinLogger
 import io.javalin.http.context.body
-import io.javalin.plugin.json.FromJsonMapper
 import io.javalin.plugin.json.JavalinJackson
-import io.javalin.plugin.json.JavalinJson
-import io.javalin.plugin.json.ToJsonMapper
+import io.javalin.plugin.json.JsonMapper
 import io.javalin.testing.NonSerializableObject
 import io.javalin.testing.SerializableObject
 import io.javalin.testing.TestUtil
@@ -37,26 +35,18 @@ class TestJson {
     fun `json-mapper throws when mapping unmappable object to json`() = TestUtil.test { app, http ->
         app.get("/hello") { ctx -> ctx.json(NonSerializableObject()) }
         assertThat(http.get("/hello").status).isEqualTo(500)
-        assertThat(http.getBody("/hello")).isEqualTo("""{
-                |    "title": "Internal server error",
-                |    "status": 500,
-                |    "type": "https://javalin.io/documentation#internalservererrorresponse",
-                |    "details": {}
-                |}""".trimMargin())
+        assertThat(http.getBody("/hello")).contains(""""title": "Internal server error"""")
     }
 
     @Test
     fun `json-mapper maps json to object`() = TestUtil.test { app, http ->
-        app.post("/hello") { ctx ->
-            ctx.body<SerializableObject>()
-            ctx.result("success")
-        }
-        val jsonString = JavalinJackson.toJson(SerializableObject())
-        assertThat(http.post("/hello").body(jsonString).asString().body).isEqualTo("success")
+        app.post("/hello") { it.result(it.body<SerializableObject>().value1) }
+        val jsonString = JavalinJackson().toJson(SerializableObject())
+        assertThat(http.post("/hello").body(jsonString).asString().body).isEqualTo("FirstValue")
     }
 
     @Test
-    fun `invalid json is mapped to BadRequestResponse`() = TestUtil.test { app, http ->
+    fun `invalid json is handled by Validator`() = TestUtil.test { app, http ->
         app.get("/hello") { it.body<NonSerializableObject>() }
         assertThat(http.get("/hello").status).isEqualTo(400)
         val response = http.getBody("/hello").replace("\\s".toRegex(), "")
@@ -64,68 +54,57 @@ class TestJson {
     }
 
     @Test
-    fun `custom silly toJsonMapper works`() = TestUtil.test { app, http ->
-        JavalinJson.toJsonMapper = object : ToJsonMapper {
-            override fun map(obj: Any) = "Silly mapper"
+    fun `custom silly JSON mapper works`() {
+        val sillyMapper = object : JsonMapper {
+            override fun toJson(obj: Any): String = "Silly mapper"
+        }
+        TestUtil.test(Javalin.create { it.jsonMapper(sillyMapper) }) { app, http ->
+            app.get("/") { ctx -> ctx.json("Test") }
+            assertThat(http.getBody("/")).isEqualTo("Silly mapper")
         }
-        app.get("/") { ctx -> ctx.json("Test") }
-        assertThat(http.getBody("/")).isEqualTo("Silly mapper")
     }
 
     @Test
-    fun `custom gson toJsonMapper mapper works`() = TestUtil.test { app, http ->
-        val gson = GsonBuilder().create()
-        JavalinJson.toJsonMapper = object : ToJsonMapper {
-            override fun map(obj: Any) = gson.toJson(obj)
-        }
-        app.get("/") { ctx -> ctx.json(SerializableObject()) }
-        assertThat(http.getBody("/")).isEqualTo(gson.toJson(SerializableObject()))
+    fun `empty mapper throws exception`() = TestUtil.test(Javalin.create { it.jsonMapper(object : JsonMapper {}) }) { app, http ->
+        app.get("/") { ctx -> ctx.json("Test") }
+        assertThat(http.getBody("/")).isEqualTo("")
+        assertThat(http.get("/").status).isEqualTo(500)
     }
 
-    @Suppress("UNCHECKED_CAST")
     @Test
-    fun `custom silly fromJsonMapper works`() = TestUtil.test { app, http ->
-        val sillyString = "Silly string"
-        JavalinJson.fromJsonMapper = object : FromJsonMapper {
-            override fun <T> map(json: String, targetClass: Class<T>) = sillyString as T
+    fun `empty mapper logs proper error message`() = TestUtil.test(Javalin.create { it.jsonMapper(object : JsonMapper {}) }) { app, http ->
+        val log = TestUtil.captureStdOut {
+            JavalinLogger.enabled = true
+            app.get("/") { it.json("Test") }
+            http.getBody("/")
         }
-        app.post("/") { ctx ->
-            if (sillyString == ctx.bodyAsClass(String::class.java)) {
-                ctx.result(sillyString)
-            }
-        }
-        assertThat(Unirest.post("${http.origin}/").body("{}").asString().body).isEqualTo(sillyString)
+        assertThat(log).contains("JsonMapper#toJson not implemented")
     }
 
     @Test
-    fun `custom gson fromJsonMapper works`() = TestUtil.test { app, http ->
+    fun `custom GSON mapper works`() {
         val gson = GsonBuilder().create()
-        JavalinJson.fromJsonMapper = object : FromJsonMapper {
-            override fun <T> map(json: String, targetClass: Class<T>) = gson.fromJson(json, targetClass)
+        val gsonMapper = object : JsonMapper {
+            override fun <T> fromJson(json: String, targetClass: Class<T>): T = gson.fromJson(json, targetClass)
+            override fun toJson(obj: Any) = gson.toJson(obj)
         }
-        app.post("/") { ctx ->
-            ctx.bodyAsClass(SerializableObject::class.java)
-            ctx.result("success")
+        TestUtil.test(Javalin.create { it.jsonMapper(gsonMapper) }) { app, http ->
+            app.get("/") { ctx -> ctx.json(SerializableObject()) }
+            assertThat(http.getBody("/")).isEqualTo(gson.toJson(SerializableObject()))
+            app.post("/") { ctx ->
+                ctx.bodyAsClass(SerializableObject::class.java)
+                ctx.result("success")
+            }
+            assertThat(http.post("/").body(gson.toJson(SerializableObject())).asString().body).isEqualTo("success")
         }
-        assertThat(http.post("/").body(gson.toJson(SerializableObject())).asString().body).isEqualTo("success")
-    }
-
-    @Test
-    fun `can use JavalinJson as an object-mapper`() {
-        val mapped = JavalinJson.toJson(SerializableObject())
-        val mappedBack = JavalinJson.fromJson(mapped, SerializableObject::class.java)
-        assertThat(SerializableObject().value1).isEqualTo(mappedBack.value1)
-        assertThat(SerializableObject().value2).isEqualTo(mappedBack.value2)
     }
 
-    data class SerializableDataClass(val value1: String, val value2: String)
+    data class SerializableDataClass(val value1: String = "Default1", val value2: String)
 
     @Test
-    fun `can use JavalinJson with a custom object-mapper on a kotlin data class`() {
-        val jacksonKtEnabledObjectMapper = JavalinJackson.defaultObjectMapper()
-        JavalinJackson.configure(jacksonKtEnabledObjectMapper) // override mapper
-        val mapped = JavalinJson.toJson(SerializableDataClass("First value", "Second value"))
-        val mappedBack = JavalinJson.fromJson(mapped, SerializableDataClass::class.java)
+    fun `can use JavalinJackson with a custom object-mapper on a kotlin data class`() {
+        val mapped = JavalinJackson().toJson(SerializableDataClass("First value", "Second value"))
+        val mappedBack = JavalinJackson().fromJson(mapped, SerializableDataClass::class.java)
         assertThat("First value").isEqualTo(mappedBack.value1)
         assertThat("Second value").isEqualTo(mappedBack.value2)
     }
diff --git a/javalin/src/test/java/io/javalin/TestMultipartForms.kt b/javalin/src/test/java/io/javalin/TestMultipartForms.kt
index 6b0e830a..ab7f7405 100644
--- a/javalin/src/test/java/io/javalin/TestMultipartForms.kt
+++ b/javalin/src/test/java/io/javalin/TestMultipartForms.kt
@@ -10,12 +10,10 @@ import io.javalin.http.context.formParam
 import io.javalin.plugin.json.JavalinJackson
 import io.javalin.testing.TestUtil
 import io.javalin.testing.UploadInfo
-import okhttp3.MediaType
 import okhttp3.MediaType.Companion.toMediaTypeOrNull
 import okhttp3.MultipartBody
 import okhttp3.OkHttpClient
 import okhttp3.Request
-import okhttp3.RequestBody
 import okhttp3.RequestBody.Companion.asRequestBody
 import org.apache.commons.io.IOUtils
 import org.assertj.core.api.Assertions.assertThat
@@ -56,7 +54,8 @@ class TestMultipartForms {
         val response = http.post("/test-upload")
                 .field("upload", uploadFile)
                 .asString()
-        val uploadInfo = JavalinJackson.fromJson(response.body, UploadInfo::class.java)
+
+        val uploadInfo = JavalinJackson().fromJson(response.body, UploadInfo::class.java)
         assertThat(uploadInfo.size).isEqualTo(uploadFile.length())
         assertThat(uploadInfo.filename).isEqualTo(uploadFile.name)
         assertThat(uploadInfo.contentType).isEqualTo("application/octet-stream")
@@ -73,7 +72,7 @@ class TestMultipartForms {
         val response = http.post("/test-upload")
                 .field("upload", uploadFile, "image/png")
                 .asString()
-        val uploadInfo = JavalinJackson.fromJson(response.body, UploadInfo::class.java)
+        val uploadInfo = JavalinJackson().fromJson(response.body, UploadInfo::class.java)
         assertThat(uploadInfo.size).isEqualTo(uploadFile.length())
         assertThat(uploadInfo.filename).isEqualTo(uploadFile.name)
         assertThat(uploadInfo.contentType).isEqualTo("image/png")
diff --git a/javalin/src/test/java/io/javalin/TestResponse.kt b/javalin/src/test/java/io/javalin/TestResponse.kt
index da9b4ed6..2bf9aa00 100644
--- a/javalin/src/test/java/io/javalin/TestResponse.kt
+++ b/javalin/src/test/java/io/javalin/TestResponse.kt
@@ -20,7 +20,6 @@ import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
 import java.io.File
 import java.util.*
-import javax.servlet.http.Cookie
 
 class TestResponse {
 
diff --git a/javalin/src/test/java/io/javalin/TestValidation.kt b/javalin/src/test/java/io/javalin/TestValidation.kt
index c9b463ed..c2d77729 100644
--- a/javalin/src/test/java/io/javalin/TestValidation.kt
+++ b/javalin/src/test/java/io/javalin/TestValidation.kt
@@ -20,7 +20,6 @@ import io.javalin.http.context.formParam
 import io.javalin.http.context.pathParam
 import io.javalin.http.context.queryParam
 import io.javalin.plugin.json.JavalinJackson
-import io.javalin.plugin.json.JavalinJson
 import io.javalin.testing.SerializableObject
 import io.javalin.testing.TestUtil
 import org.assertj.core.api.Assertions.assertThat
@@ -104,9 +103,10 @@ class TestValidation {
         assertThat(http.get("/duration?from=abc").status).isEqualTo(500)
     }
 
+    val timeModuleMapper by lazy { JavalinJackson(ObjectMapper().apply { registerModule(JavaTimeModule()) }) }
+
     @Test
-    fun `custom converter works`() = TestUtil.test { app, http ->
-        JavalinJackson.configure(ObjectMapper().apply { registerModule(JavaTimeModule()) })
+    fun `custom converter works`() = TestUtil.test(Javalin.create { it.jsonMapper(timeModuleMapper) }) { app, http ->
         JavalinValidation.register(Instant::class.java) { Instant.ofEpochMilli(it.toLong()) }
         app.get("/instant") { ctx ->
             val fromDate = ctx.queryParam<Instant>("from").get()
@@ -158,8 +158,8 @@ class TestValidation {
                 .get()
             ctx.result(obj.value1)
         }
-        val invalidJson = JavalinJson.toJson(SerializableObject())
-        val validJson = JavalinJson.toJson(SerializableObject().apply {
+        val invalidJson = JavalinJackson().toJson(SerializableObject())
+        val validJson = JavalinJackson().toJson(SerializableObject().apply {
             value1 = "Bananas"
         })
 
@@ -186,7 +186,7 @@ class TestValidation {
             {"message":"UnnamedFieldCheck1","args":{},"value":{"value1":"FirstValue","value2":"SecondValue"}},
             {"message":"UnnamedFieldCheck2","args":{},"value":{"value1":"First Value","value2":"SecondValue"}}],
             "named_field":[{"message":"NamedFieldCheck3","args":{},"value":{"value1":"FirstValue","value2":"SecondValue"}}]}""".replace("\\s".toRegex(), "")
-        val response = http.post("/json").body(JavalinJson.toJson(SerializableObject())).asString().body
+        val response = http.post("/json").body(JavalinJackson().toJson(SerializableObject())).asString().body
         assertThat(response).isEqualTo(expected)
     }
 
diff --git a/javalin/src/test/java/io/javalin/TestWebSocket.kt b/javalin/src/test/java/io/javalin/TestWebSocket.kt
index 31ad48c1..1c680059 100644
--- a/javalin/src/test/java/io/javalin/TestWebSocket.kt
+++ b/javalin/src/test/java/io/javalin/TestWebSocket.kt
@@ -10,7 +10,7 @@ import com.mashape.unirest.http.Unirest
 import io.javalin.apibuilder.ApiBuilder.ws
 import io.javalin.core.util.Header
 import io.javalin.http.UnauthorizedResponse
-import io.javalin.plugin.json.JavalinJson
+import io.javalin.plugin.json.JavalinJackson
 import io.javalin.testing.SerializableObject
 import io.javalin.testing.TestUtil
 import io.javalin.testing.TypedException
@@ -26,7 +26,6 @@ import org.java_websocket.handshake.ServerHandshake
 import org.junit.Test
 import java.net.URI
 import java.time.Duration
-import java.util.*
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.ConcurrentLinkedQueue
 import java.util.concurrent.TimeoutException
@@ -149,10 +148,10 @@ class TestWebSocket {
     @Test
     fun `receive and send json messages`() = TestUtil.test { app, _ ->
         val clientMessage = SerializableObject().apply { value1 = "test1"; value2 = "test2" }
-        val clientMessageJson = JavalinJson.toJson(clientMessage)
+        val clientMessageJson = JavalinJackson().toJson(clientMessage)
 
         val serverMessage = SerializableObject().apply { value1 = "echo1"; value2 = "echo2" }
-        val serverMessageJson = JavalinJson.toJson(serverMessage)
+        val serverMessageJson = JavalinJackson().toJson(serverMessage)
 
         var receivedJson: String? = null
         var receivedMessage: SerializableObject? = null
diff --git a/javalin/src/test/java/io/javalin/examples/HelloWorldGson.java b/javalin/src/test/java/io/javalin/examples/HelloWorldGson.java
index 67868a61..4fc41ea4 100644
--- a/javalin/src/test/java/io/javalin/examples/HelloWorldGson.java
+++ b/javalin/src/test/java/io/javalin/examples/HelloWorldGson.java
@@ -9,18 +9,29 @@ package io.javalin.examples;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import io.javalin.Javalin;
-import io.javalin.plugin.json.JavalinJson;
+import io.javalin.plugin.json.JsonMapper;
 import java.util.Arrays;
+import org.jetbrains.annotations.NotNull;
 
 public class HelloWorldGson {
 
     public static void main(String[] args) {
 
         Gson gson = new GsonBuilder().create();
-        JavalinJson.setFromJsonMapper(gson::fromJson);
-        JavalinJson.setToJsonMapper(gson::toJson);
-
-        Javalin app = Javalin.create().start(7070);
+        JsonMapper gsonMapper = new JsonMapper() {
+            @NotNull
+            @Override
+            public String toJson(@NotNull Object obj) {
+                return gson.toJson(obj);
+            }
+            @NotNull
+            @Override
+            public <T> T fromJson(@NotNull String json, @NotNull Class<T> targetClass) {
+                return gson.fromJson(json, targetClass);
+            }
+        };
+
+        Javalin app = Javalin.create(config -> config.jsonMapper(gsonMapper)).start(7070);
         app.get("/", ctx -> ctx.json(Arrays.asList("a", "b", "c")));
 
     }
diff --git a/javalin/src/test/java/io/javalin/testing/TestUtil.java b/javalin/src/test/java/io/javalin/testing/TestUtil.java
index 3dc74e24..a3b5d19c 100644
--- a/javalin/src/test/java/io/javalin/testing/TestUtil.java
+++ b/javalin/src/test/java/io/javalin/testing/TestUtil.java
@@ -6,15 +6,10 @@
 
 package io.javalin.testing;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.mashape.unirest.http.HttpMethod;
 import io.javalin.Javalin;
 import io.javalin.core.util.JavalinLogger;
 import io.javalin.http.Handler;
-import io.javalin.plugin.json.FromJsonMapper;
-import io.javalin.plugin.json.JavalinJackson;
-import io.javalin.plugin.json.JavalinJson;
-import io.javalin.plugin.json.ToJsonMapper;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 
@@ -24,8 +19,6 @@ public class TestUtil {
 
     public static void test(Javalin javalin, ThrowingBiConsumer<Javalin, HttpUtil> test) {
         JavalinLogger.enabled = false;
-        JavalinJson.setToJsonMapper(JavalinJackson.INSTANCE.getDefaultToMapper());
-        JavalinJson.setFromJsonMapper(JavalinJackson.INSTANCE.getDefaultFromMapper());
         javalin.start(0);
         HttpUtil http = new HttpUtil(javalin.port());
         test.accept(javalin, http);
diff --git a/javalin/src/test/kotlin/io/javalin/examples/HelloWorldGson.kt b/javalin/src/test/kotlin/io/javalin/examples/HelloWorldGson.kt
index 1060a003..6096e956 100644
--- a/javalin/src/test/kotlin/io/javalin/examples/HelloWorldGson.kt
+++ b/javalin/src/test/kotlin/io/javalin/examples/HelloWorldGson.kt
@@ -8,24 +8,19 @@ package io.javalin.examples
 
 import com.google.gson.GsonBuilder
 import io.javalin.Javalin
-import io.javalin.plugin.json.FromJsonMapper
-import io.javalin.plugin.json.JavalinJson
-import io.javalin.plugin.json.ToJsonMapper
+import io.javalin.plugin.json.JsonMapper
 import java.util.*
 
 fun main() {
 
     val gson = GsonBuilder().create()
 
-    JavalinJson.fromJsonMapper = object : FromJsonMapper {
-        override fun <T> map(json: String, targetClass: Class<T>) = gson.fromJson(json, targetClass)
+    val gsonMapper = object : JsonMapper {
+        override fun <T> fromJson(json: String, targetClass: Class<T>): T = gson.fromJson(json, targetClass)
+        override fun toJson(obj: Any) = gson.toJson(obj)
     }
 
-    JavalinJson.toJsonMapper = object : ToJsonMapper {
-        override fun map(obj: Any): String = gson.toJson(obj)
-    }
-
-    val app = Javalin.create().start(7070)
+    val app = Javalin.create { it.jsonMapper(gsonMapper) }.start(7070)
     app.get("/") { ctx -> ctx.json(Arrays.asList("a", "b", "c")) }
 
 }