Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Jackson ObjectMapper provided by Akka #9494

Merged
merged 10 commits into from
Jul 25, 2019
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
*/

package play.it.libs.json

import java.io.ByteArrayInputStream
import java.time.Instant
import java.util.Optional
import java.util.OptionalInt

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import org.specs2.mutable.Specification
import org.specs2.specification.Scope
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.mvc.Request
import play.core.test.FakeRequest
import play.libs.Json
import play.mvc.Http
import play.mvc.Http.RequestBody

// Use an `ObjectMapper` which overrides some defaults
class PlayBindingNameJavaJsonSpec extends JavaJsonSpec {
override val createObjectMapper: ObjectMapper = GuiceApplicationBuilder()
// should be able to use `.play.` namespace to override configurations
// for this `ObjectMapper`.
.configure("akka.serialization.jackson.play.serialization-features.WRITE_DURATIONS_AS_TIMESTAMPS" -> false)
.build()
.injector
.instanceOf[ObjectMapper]

"ObjectMapper" should {
"respect the custom configuration" in new JsonScope {
Json.mapper().isEnabled(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) must beFalse
Copy link
Member

@mkurz mkurz Mar 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is wrong. It tests for WRITE_DATES_WITH_ZONE_ID but WRITE_DURATIONS_AS_TIMESTAMPS is configured. Makes no sense. No matter if I set WRITE_DURATIONS_AS_TIMESTAMPS to true or false (like here) the test always passes...

}
}
}

// The dependency injected `ObjectMapper`
class ApplicationJavaJsonSpec extends JavaJsonSpec {
marcospereira marked this conversation as resolved.
Show resolved Hide resolved
override val createObjectMapper: ObjectMapper = GuiceApplicationBuilder().build().injector.instanceOf[ObjectMapper]
}

// Classic static `ObjectMapper` from play.libs.Json
class StaticJavaJsonSpec extends JavaJsonSpec {
override val createObjectMapper: ObjectMapper = Json.newDefaultMapper()
}

trait JavaJsonSpec extends Specification {

sequential

def createObjectMapper: ObjectMapper

private[json] class JsonScope(val mapper: ObjectMapper = createObjectMapper) extends Scope {
val testJsonString =
"""{
| "foo" : "bar",
| "bar" : "baz",
| "instant" : 1425435861,
| "optNumber" : 55555,
| "optionalInt" : 12345,
| "a" : 2.5,
| "copyright" : "\u00a9",
| "baz" : [ 1, 2, 3 ]
|}""".stripMargin.replaceAll("\r?\n", System.lineSeparator)

val testJsonInputStream = new ByteArrayInputStream(testJsonString.getBytes("UTF-8"))

val testJson = mapper.createObjectNode()
testJson
.put("foo", "bar")
.put("bar", "baz")
.put("instant", 1425435861)
.put("optNumber", 55555)
.put("optionalInt", 12345)
.put("a", 2.5)
.put("copyright", "\u00a9") // copyright symbol
.set("baz", mapper.createArrayNode().add(1).add(2).add(3))

Json.setObjectMapper(mapper)
}

"Json" should {

"use the correct object mapper" in new JsonScope {
Json.mapper() must_== mapper
}

"parse" in {

"from string" in new JsonScope {
Json.parse(testJsonString) must_== testJson
}

"from UTF-8 byte array" in new JsonScope {
Json.parse(testJsonString.getBytes("UTF-8")) must_== testJson
}

"from InputStream" in new JsonScope {
Json.parse(testJsonInputStream) must_== testJson
}
}

"stringify" in {

"stringify" in new JsonScope {
Json.stringify(testJson) must_== Json.stringify(Json.parse(testJsonString))
}

"asciiStringify" in new JsonScope {
val resultString = Json.stringify(Json.parse(testJsonString)).replace("\u00a9", "\\u00A9")
Json.asciiStringify(testJson) must_== resultString
}

"prettyPrint" in new JsonScope {
Json.prettyPrint(testJson) must_== testJsonString
}

"serialize Java Optional fields" in new JsonScope {
val optNumber = Optional.of[Integer](55555)
val optInt = OptionalInt.of(12345)

// The configured mapper should be able to handle optional values
Json.mapper().writeValueAsString(optNumber) must_== "55555"
Json.mapper().writeValueAsString(optInt) must_== "12345"
}

"serialize Java Time field" in new JsonScope {
val instant: Instant = Instant.ofEpochSecond(1425435861)

// The configured mapper should be able to handle Java Time fields
Json.mapper().writeValueAsString(instant) must_== """"2015-03-04T02:24:21Z""""
}
}

"when deserializing to a POJO" should {

"deserialize from request body" in new JsonScope(createObjectMapper) {
val validRequest: Request[Http.RequestBody] =
Request[Http.RequestBody](FakeRequest(), new RequestBody(testJson))
val javaPOJO = validRequest.body.parseJson(classOf[JavaPOJO]).get()

javaPOJO.getBar must_== "baz"
javaPOJO.getFoo must_== "bar"
javaPOJO.getInstant must_== Instant.ofEpochSecond(1425435861L)
javaPOJO.getOptNumber must_== Optional.of(55555)
javaPOJO.getOptionalInt must_== OptionalInt.of(12345)
}

"deserialize even if there are missing fields" in new JsonScope(createObjectMapper) {
val testJsonMissingFields: Request[Http.RequestBody] =
Request[Http.RequestBody](FakeRequest(), new RequestBody(mapper.createObjectNode()))
testJsonMissingFields.body.parseJson(classOf[JavaPOJO]).get().getBar must_== null
}

"return empty when request body is not a JSON" in new JsonScope(createObjectMapper) {
val testNotJsonBody: Request[Http.RequestBody] =
Request[Http.RequestBody](FakeRequest(), new RequestBody("foo"))
testNotJsonBody.body.parseJson(classOf[JavaPOJO]) must_== Optional.empty()
}

"ignore unknown fields" in new JsonScope(createObjectMapper) {
val javaPOJO = Json.fromJson(testJson, classOf[JavaPOJO])
javaPOJO.getBar must_== "baz"
javaPOJO.getFoo must_== "bar"
javaPOJO.getInstant must_== Instant.ofEpochSecond(1425435861L)
javaPOJO.getOptNumber must_== Optional.of(55555)
}
}

"when serializing from a POJO" should {
"serialize to a response body" in new JsonScope(createObjectMapper) {
val pojo = new JavaPOJO(
"Foo String",
"Bar String",
Instant.ofEpochSecond(1425435861),
Optional.of[Integer](55555),
OptionalInt.of(12345)
)
val jsonNode: JsonNode = Json.toJson(pojo)

// Regular fields
jsonNode.get("foo").asText() must_== "Foo String"
jsonNode.get("bar").asText() must_== "Bar String"

// Optional fields
jsonNode.get("optNumber").asText() must_== "55555"
jsonNode.get("optionalInt").asText() must_== "12345"

// Java Time fields
jsonNode.get("instant").asText() must_== "2015-03-04T02:24:21Z"
}

"include null fields" in new JsonScope(createObjectMapper) {
val pojo = new JavaPOJO(
null, // foo
"Bar String", // bar
Instant.ofEpochSecond(1425435861),
Optional.of[Integer](55555),
OptionalInt.of(12345)
)
val jsonNode: JsonNode = Json.toJson(pojo)

jsonNode.has("foo") must beTrue
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@
* Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
*/

package play.libs;
package play.it.libs.json;

import java.time.Instant;
import java.util.Optional;
import java.util.OptionalInt;

public class JavaPOJO {

private String foo;
private String bar;
private Instant instant;
private Optional<Integer> optNumber;
private OptionalInt optionalInt;

public JavaPOJO() {
// empty constructor useful for Jackson
}

public JavaPOJO(String foo, String bar, Instant instant, Optional<Integer> optNumber, OptionalInt optionalInt) {
this.foo = foo;
this.bar = bar;
this.instant = instant;
this.optNumber = optNumber;
this.optionalInt = optionalInt;
}

public String getFoo() {
return foo;
Expand Down Expand Up @@ -45,4 +59,12 @@ public Optional<Integer> getOptNumber() {
public void setOptNumber(Optional<Integer> optNumber) {
this.optNumber = optNumber;
}

public OptionalInt getOptionalInt() {
return optionalInt;
}

public void setOptionalInt(OptionalInt optionalInt) {
this.optionalInt = optionalInt;
}
}
18 changes: 12 additions & 6 deletions core/play-java/src/main/scala/play/core/ObjectMapperModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

package play.core

import akka.actor.ActorSystem
import akka.serialization.jackson.JacksonObjectMapperProvider
import com.fasterxml.jackson.databind.ObjectMapper
import play.api.inject._
import play.libs.Json

import javax.inject._

import scala.concurrent.Future
Expand All @@ -24,14 +25,18 @@ class ObjectMapperModule
)

@Singleton
class ObjectMapperProvider @Inject()(lifecycle: ApplicationLifecycle) extends Provider[ObjectMapper] {
class ObjectMapperProvider @Inject()(lifecycle: ApplicationLifecycle, actorSystem: ActorSystem)
extends Provider[ObjectMapper] {

private val BINDING_NAME = "play"

lazy val get: ObjectMapper = {
val objectMapper = Json.newDefaultMapper()
Json.setObjectMapper(objectMapper)
val mapper = JacksonObjectMapperProvider.get(actorSystem).getOrCreate(BINDING_NAME, Option.empty)
Json.setObjectMapper(mapper)
lifecycle.addStopHook { () =>
Future.successful(Json.setObjectMapper(null))
}
objectMapper
mapper
}
}

Expand All @@ -40,7 +45,8 @@ class ObjectMapperProvider @Inject()(lifecycle: ApplicationLifecycle) extends Pr
*/
trait ObjectMapperComponents {

def actorSystem: ActorSystem
def applicationLifecycle: ApplicationLifecycle

lazy val objectMapper: ObjectMapper = new ObjectMapperProvider(applicationLifecycle).get
lazy val objectMapper: ObjectMapper = new ObjectMapperProvider(applicationLifecycle, actorSystem).get
}
11 changes: 10 additions & 1 deletion core/play/src/main/java/play/libs/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,20 @@ public class Json {
private static final ObjectMapper defaultObjectMapper = newDefaultMapper();
private static volatile ObjectMapper objectMapper = null;

/**
* Creates an {@link ObjectMapper} with the default configuration for Play.
*
* @return an {@link ObjectMapper} with some modules enabled.
* @deprecated Deprecated as of 2.8.0. Inject an {@link ObjectMapper} instead.
*/
@Deprecated
public static ObjectMapper newDefaultMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());
mapper.registerModule(new JavaTimeModule());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return mapper;
}

Expand Down
4 changes: 2 additions & 2 deletions core/play/src/main/scala/play/api/libs/concurrent/Akka.scala
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ object ActorSystemProvider {
*
* @return The ActorSystem and a function that can be used to stop it.
*/
@deprecated("Use setup(ClassLoader, Configuration, Setup*) instead", "2.8.0")
@deprecated("Use start(ClassLoader, Configuration, Setup*) instead", "2.8.0")
protected[ActorSystemProvider] def start(classLoader: ClassLoader, config: Configuration): ActorSystem = {
start(classLoader, config, Seq.empty: _*)
}
Expand All @@ -149,7 +149,7 @@ object ActorSystemProvider {
*
* @return The ActorSystem and a function that can be used to stop it.
*/
@deprecated("Use setup(ClassLoader, Configuration, Setup*) instead", "2.8.0")
@deprecated("Use start(ClassLoader, Configuration, Setup*) instead", "2.8.0")
protected[ActorSystemProvider] def start(
classLoader: ClassLoader,
config: Configuration,
Expand Down