Skip to content

Commit

Permalink
Merge pull request #10495 from playframework/rgc/json-fix
Browse files Browse the repository at this point in the history
Fix Json issues: parse on form and tailrec deser
  • Loading branch information
mergify[bot] committed Oct 26, 2020
2 parents b71352a + e0bc629 commit ce6ea55
Show file tree
Hide file tree
Showing 15 changed files with 910 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (C) Lightbend Inc. <https://www.lightbend.com>
*/

package play.it.http.parsing

import java.util.concurrent.TimeUnit

import akka.stream.Materializer
import akka.stream.scaladsl.Source
import akka.util.ByteString
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import org.specs2.execute.Failure
import org.specs2.matcher.Matchers
import play.api.Application
import play.api.Configuration
import play.api.test._
import play.libs.F
import play.mvc.BodyParser
import play.mvc.Http
import play.mvc.Result
import play.test.Helpers

class JacksonJsonBodyParserSpec extends PlaySpecification with Matchers {

// Jackson Json support in Play relies on static
// global variables so these tests must run sequentially
sequential

private def tolerantJsonBodyParser(implicit app: Application): BodyParser[JsonNode] =
app.injector.instanceOf(classOf[BodyParser.TolerantJson])

"The JSON body parser" should {
def parse(json: String)(
implicit mat: Materializer,
app: Application
): F.Either[Result, JsonNode] = {
val encoding: String = "utf-8"
val bodyParser = tolerantJsonBodyParser
val fakeRequest: Http.RequestHeader = Helpers.fakeRequest().header(CONTENT_TYPE, "application/json").build()
await(
bodyParser(fakeRequest).asScala().run(Source.single(ByteString(json.getBytes(encoding))))
)
}

"uses JacksonJsonNodeModule" in new WithApplication() {
private val mapper: ObjectMapper = implicitly[Application].injector.instanceOf[ObjectMapper]
mapper.getRegisteredModuleIds.contains("play.utils.JacksonJsonNodeModule") must_== true
}

"parse a simple JSON body with custom Jackson json-read-features" in new WithApplication(
guiceBuilder =>
guiceBuilder.configure(
"akka.serialization.jackson.play.json-read-features.ALLOW_SINGLE_QUOTES" -> "true"
)
) {

val configuration: Configuration = implicitly[Application].configuration
configuration.get[Boolean]("akka.serialization.jackson.play.json-read-features.ALLOW_SINGLE_QUOTES") must beTrue

val either: F.Either[Result, JsonNode] = parse("""{ 'field1':'value1' }""")
either.left.ifPresent(verboseFailure)
either.right.get().get("field1").asText() must_=== "value1"
}

"parse very deep JSON bodies" in new WithApplication() {
val depth = 50000
private val either: F.Either[Result, JsonNode] = parse(s"""{"foo": ${"[" * depth} "asdf" ${"]" * depth} }""")
private var node: JsonNode = either.right.get().at("/foo")
while (node.isArray) {
node = node.get(0)
}

node.asText() must_== "asdf"
}

}

def verboseFailure(result: Result)(implicit mat: Materializer): Failure = {
val errorMessage = s"""Parse failure. Play-produced error HTML page:
| ${resultToString(result)}
|""".stripMargin
failure(errorMessage)
}

def resultToString(r: Result)(implicit mat: Materializer): String = {
r.body()
.consumeData(mat)
.toCompletableFuture
.get(6, TimeUnit.SECONDS)
.utf8String
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package play.it.libs.json

import java.io.ByteArrayInputStream
import java.math.BigInteger
import java.math.MathContext._
import java.time.Instant
import java.util.Optional
import java.util.OptionalInt
Expand Down Expand Up @@ -68,7 +70,8 @@ trait JavaJsonSpec extends Specification {
| "instant" : 1425435861,
| "optNumber" : 55555,
| "optionalInt" : 12345,
| "a" : 2.5,
| "float" : 2.5,
| "double" : 1.7976931348623157E308,
| "copyright" : "\u00a9",
| "baz" : [ 1, 2, 3 ]
|}""".stripMargin.replaceAll("\r?\n", System.lineSeparator)
Expand All @@ -82,8 +85,9 @@ trait JavaJsonSpec extends Specification {
.put("instant", 1425435861)
.put("optNumber", 55555)
.put("optionalInt", 12345)
.put("a", 2.5)
.put("copyright", "\u00a9") // copyright symbol
.put("float", 2.5)
.put("double", 1.7976931348623157e308) // Double.MaxValue
.put("copyright", "\u00a9") // copyright symbol
.set("baz", mapper.createArrayNode().add(1).add(2).add(3))

Json.setObjectMapper(mapper)
Expand Down
7 changes: 7 additions & 0 deletions core/play-java/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ play {
enabled += "play.routing.RoutingDslModule"
}
}

akka.serialization.jackson.play {
# The "play" ObjectMapper uses the default Jackson Modules configured in akka.serialization.jackson.jackson-modules
# but also must use "play.utils.JacksonJsonNodeModule".
# Note: the syntax ${reference.to.an.array} ["another-value"] is lightbend config for concatenating arrays.
jackson-modules = ${akka.serialization.jackson.jackson-modules} ["play.utils.JacksonJsonNodeModule"]
}
15 changes: 10 additions & 5 deletions core/play-java/src/main/scala/play/core/ObjectMapperModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

package play.core

import scala.concurrent.Future

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

import scala.concurrent.Future

/**
* Module that injects an object mapper to the JSON library on start and on stop.
Expand All @@ -24,13 +24,18 @@ class ObjectMapperModule
bind[ObjectMapper].toProvider[ObjectMapperProvider].eagerly()
)

object ObjectMapperProvider {
val BINDING_NAME = "play"
}
@Singleton
class ObjectMapperProvider @Inject() (lifecycle: ApplicationLifecycle, actorSystem: ActorSystem)
extends Provider[ObjectMapper] {
private val BINDING_NAME = "play"

lazy val get: ObjectMapper = {
val mapper = JacksonObjectMapperProvider.get(actorSystem).getOrCreate(BINDING_NAME, Option.empty)
val mapper =
JacksonObjectMapperProvider
.get(actorSystem)
.getOrCreate(ObjectMapperProvider.BINDING_NAME, Option.empty)
Json.setObjectMapper(mapper)
lifecycle.addStopHook { () =>
Future.successful(Json.setObjectMapper(null))
Expand Down
8 changes: 7 additions & 1 deletion core/play/src/main/java/play/libs/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.fasterxml.jackson.module.scala.DefaultScalaModule;
import play.utils.JsonNodeDeserializer;

/** Helper functions to handle JsonNode values. */
public class Json {
Expand All @@ -33,12 +35,16 @@ public class Json {
*/
@Deprecated
public static ObjectMapper newDefaultMapper() {
SimpleModule module = new SimpleModule();
module.<JsonNode>addDeserializer(JsonNode.class, new JsonNodeDeserializer());

return JsonMapper.builder()
.addModules(
new Jdk8Module(),
new JavaTimeModule(),
new ParameterNamesModule(),
new DefaultScalaModule())
new DefaultScalaModule(),
module)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false)
Expand Down
Loading

0 comments on commit ce6ea55

Please sign in to comment.