diff --git a/build.sbt b/build.sbt index 49905a1e..77768027 100644 --- a/build.sbt +++ b/build.sbt @@ -21,19 +21,12 @@ def specs2(scalaVersion: String) = ("org.specs2" %% s"specs2-$n" % "4.23.0") % Test } -val jacksonDatabindVersion = "2.20.1" -val jacksonDatabind = Seq( - "com.fasterxml.jackson.core" % "jackson-databind" % jacksonDatabindVersion -) - -val jacksonVersion = jacksonDatabindVersion +val jacksonVersion = "3.0.3" val jacksons = Seq( - "com.fasterxml.jackson.core" % "jackson-core", - "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8", - "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310", - "com.fasterxml.jackson.module" % "jackson-module-parameter-names", - "com.fasterxml.jackson.module" %% "jackson-module-scala", -).map(_ % jacksonVersion) ++ jacksonDatabind + "tools.jackson.core" % "jackson-core", + "tools.jackson.core" % "jackson-databind", + "tools.jackson.module" %% "jackson-module-scala", +).map(_ % jacksonVersion) val joda = Seq( "joda-time" % "joda-time" % "2.14.0" diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/EnvReads.scala b/play-json/jvm/src/main/scala/play/api/libs/json/EnvReads.scala index 60cde37c..1b69f580 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/EnvReads.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/EnvReads.scala @@ -26,9 +26,9 @@ import java.util.Locale import scala.util.control.NonFatal -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.ObjectNode +import tools.jackson.databind.JsonNode +import tools.jackson.databind.node.ArrayNode +import tools.jackson.databind.node.ObjectNode import play.api.libs.json.jackson.JacksonJson diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/EnvWrites.scala b/play-json/jvm/src/main/scala/play/api/libs/json/EnvWrites.scala index 23983a12..ecbcdb5b 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/EnvWrites.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/EnvWrites.scala @@ -18,7 +18,7 @@ import java.time.ZonedDateTime import java.time.{ Duration => JDuration } import java.util.Locale -import com.fasterxml.jackson.databind.JsonNode +import tools.jackson.databind.JsonNode import play.api.libs.json.jackson.JacksonJson trait EnvWrites { diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala index 38f999c9..162e8ef2 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala @@ -4,8 +4,8 @@ package play.api.libs.json -import com.fasterxml.jackson.core.StreamReadConstraints -import com.fasterxml.jackson.core.StreamWriteConstraints +import tools.jackson.core.StreamReadConstraints +import tools.jackson.core.StreamWriteConstraints import play.api.libs.json.JsonConfig.defaultMaxPlain import play.api.libs.json.JsonConfig.defaultMinPlain diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala b/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala index d0e2c047..73469022 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala @@ -14,26 +14,19 @@ import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ListBuffer -import com.fasterxml.jackson.core.JsonFactoryBuilder -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonTokenId -import com.fasterxml.jackson.core.Version -import com.fasterxml.jackson.core.json.JsonWriteFeature -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter - -import com.fasterxml.jackson.databind.Module.SetupContext -import com.fasterxml.jackson.databind._ -import com.fasterxml.jackson.databind.`type`.TypeFactory -import com.fasterxml.jackson.databind.deser.Deserializers -import com.fasterxml.jackson.databind.json.JsonMapper -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.databind.ser.Serializers -import com.fasterxml.jackson.databind.util.TokenBuffer -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 com.fasterxml.jackson.annotation.JsonFormat +import tools.jackson.core.{ JsonGenerator, JsonParser, JsonTokenId, Version } +import tools.jackson.core.json.{ JsonFactory, JsonWriteFeature } +import tools.jackson.databind.JacksonModule.SetupContext +import tools.jackson.databind._ +import tools.jackson.databind.`type`.TypeFactory +import tools.jackson.databind.cfg.DateTimeFeature +import tools.jackson.databind.deser.Deserializers +import tools.jackson.databind.json.JsonMapper +import tools.jackson.databind.module.SimpleModule +import tools.jackson.databind.ser.Serializers +import tools.jackson.databind.util.TokenBuffer +import tools.jackson.module.scala.DefaultScalaModule import play.api.libs.json._ @@ -66,7 +59,7 @@ sealed class PlayJsonMapperModule(jsonConfig: JsonConfig) extends SimpleModule(" // -- Serializers. -private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSerializer[JsValue] { +private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends ValueSerializer[JsValue] { import java.math.{ BigDecimal => JBigDec } private def stripTrailingZeros(bigDec: JBigDec): JBigDec = { @@ -79,7 +72,7 @@ private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSer } } - override def serialize(value: JsValue, json: JsonGenerator, provider: SerializerProvider): Unit = { + override def serialize(value: JsValue, json: JsonGenerator, ctxt: SerializationContext): Unit = { value match { case JsNumber(v) => { // Workaround #3784: Same behaviour as if JsonGenerator were @@ -89,18 +82,22 @@ private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSer val va = v.abs va <= jsonConfig.bigDecimalSerializerConfig.maxPlain && va >= jsonConfig.bigDecimalSerializerConfig.minPlain } - val stripped = stripTrailingZeros(v.bigDecimal) - val raw = if (shouldWritePlain) stripped.toPlainString else stripped.toString - - if (raw.exists(c => c == 'E' || c == '.')) - json.writeNumber(raw) - else - json match { - case tb: TokenBuffer => - tb.writeNumber(raw, true) - case _ => - json.writeNumber(raw) - } + val stripped = stripTrailingZeros(v.bigDecimal) + val (raw, rawAsBigDecimal) = if (shouldWritePlain) { + val str = stripped.toPlainString + (str, new JBigDec(str)) + } else { + (stripped.toString, stripped) + } + + json match { + case tb: TokenBuffer => + // If the JsonGenerator is a TokenBuffer, use its writeNumber specific method + val isInteger = raw.forall(c => c != 'E' && c != '.') + tb.writeNumber(raw, isInteger) + case _ => + json.writeNumber(rawAsBigDecimal) + } } case JsString(v) => json.writeString(v) @@ -109,7 +106,7 @@ private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSer case JsArray(elements) => { json.writeStartArray() elements.foreach { t => - serialize(t, json, provider) + serialize(t, json, ctxt) } json.writeEndArray() } @@ -117,8 +114,8 @@ private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSer case JsObject(values) => { json.writeStartObject() values.foreach { t => - json.writeFieldName(t._1) - serialize(t._2, json, provider) + json.writeName(t._1) + serialize(t._2, json, ctxt) } json.writeEndObject() } @@ -152,7 +149,7 @@ private[jackson] case class ReadingMap(content: ListBuffer[(String, JsValue)]) e } private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[?], jsonConfig: JsonConfig) - extends JsonDeserializer[Object] { + extends ValueDeserializer[Object] { override def isCachable: Boolean = true override def deserialize(jp: JsonParser, ctxt: DeserializationContext): JsValue = { @@ -168,7 +165,7 @@ private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[?] jp: JsonParser, parserContext: List[DeserializerContext] ): (Some[JsNumber], List[DeserializerContext]) = { - BigDecimalParser.parse(jp.getText, jsonConfig) match { + BigDecimalParser.parse(jp.getString, jsonConfig) match { case JsSuccess(bigDecimal, _) => (Some(JsNumber(bigDecimal)), parserContext) @@ -193,14 +190,14 @@ private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[?] ctxt: DeserializationContext, parserContext: List[DeserializerContext] ): JsValue = { - if (jp.getCurrentToken == null) { + if (jp.currentToken() == null) { jp.nextToken() // happens when using treeToValue (we're not parsing tokens) } - val valueAndCtx = (jp.getCurrentToken.id(): @switch) match { + val valueAndCtx = (jp.currentToken().id(): @switch) match { case JsonTokenId.ID_NUMBER_INT | JsonTokenId.ID_NUMBER_FLOAT => parseBigDecimal(jp, parserContext) - case JsonTokenId.ID_STRING => (Some(JsString(jp.getText)), parserContext) + case JsonTokenId.ID_STRING => (Some(JsString(jp.getString)), parserContext) case JsonTokenId.ID_TRUE => (Some(JsBoolean(true)), parserContext) @@ -218,7 +215,7 @@ private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[?] case JsonTokenId.ID_START_OBJECT => (None, ReadingMap(ListBuffer()) +: parserContext) - case JsonTokenId.ID_FIELD_NAME => + case JsonTokenId.ID_PROPERTY_NAME => parserContext match { case (c: ReadingMap) :: stack => (None, c.setField(jp.currentName()) +: stack) case _ => throw new RuntimeException("We should be reading map, something got wrong") @@ -248,26 +245,39 @@ private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[?] } // This is used when the root object is null, ie when deserializing "null" - override val getNullValue = JsNull + override def getNullValue(ctxt: DeserializationContext) = JsNull } private[jackson] class PlayDeserializers(jsonSettings: JsonConfig) extends Deserializers.Base { - override def findBeanDeserializer(javaType: JavaType, config: DeserializationConfig, beanDesc: BeanDescription) = { + override def findBeanDeserializer( + javaType: JavaType, + config: DeserializationConfig, + beanDescRef: BeanDescription.Supplier + ) = { val klass = javaType.getRawClass if (classOf[JsValue].isAssignableFrom(klass) || klass == JsNull.getClass) { new JsValueDeserializer(config.getTypeFactory, klass, jsonSettings) } else null } + + override def hasDeserializerFor(config: DeserializationConfig, valueType: Class[?]): Boolean = { + classOf[JsValue].isAssignableFrom(valueType) || valueType == JsNull.getClass + } } private[jackson] class PlaySerializers(jsonSettings: JsonConfig) extends Serializers.Base { - override def findSerializer(config: SerializationConfig, javaType: JavaType, beanDesc: BeanDescription) = { - val ser: Object = if (classOf[JsValue].isAssignableFrom(beanDesc.getBeanClass)) { + override def findSerializer( + config: SerializationConfig, + javaType: JavaType, + beanDescRef: BeanDescription.Supplier, + formatOverrides: JsonFormat.Value + ) = { + val ser: Object = if (classOf[JsValue].isAssignableFrom(beanDescRef.getBeanClass)) { new JsValueSerializer(jsonSettings) } else { null } - ser.asInstanceOf[JsonSerializer[Object]] + ser.asInstanceOf[ValueSerializer[Object]] } } @@ -283,51 +293,43 @@ private[play] object JacksonJson { } private[play] case class JacksonJson(defaultMapperJsonConfig: JsonConfig) { - private var currentMapper: ObjectMapper = null - private val defaultMapper: ObjectMapper = JsonMapper + private var currentMapper: JsonMapper = null + private val defaultMapper: JsonMapper = JsonMapper .builder( - new JsonFactoryBuilder() + JsonFactory + .builder() .streamReadConstraints(defaultMapperJsonConfig.streamReadConstraints) .streamWriteConstraints(defaultMapperJsonConfig.streamWriteConstraints) .build() ) .addModules( - new ParameterNamesModule(), - new Jdk8Module(), - new JavaTimeModule(), new DefaultScalaModule(), new PlayJsonMapperModule(defaultMapperJsonConfig), ) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DateTimeFeature.WRITE_DURATIONS_AS_TIMESTAMPS) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) .build() - private[play] def mapper(): ObjectMapper = if (currentMapper == null) { + private[play] def mapper(): JsonMapper = if (currentMapper == null) { defaultMapper } else { currentMapper } - private[play] def setObjectMapper(mapper: ObjectMapper): Unit = { + private[play] def setObjectMapper(mapper: JsonMapper): Unit = { this.currentMapper = mapper } - private def stringJsonGenerator(out: StringWriter) = - mapper().getFactory.createGenerator(out) - - private def stringJsonGenerator(out: OutputStream) = - mapper().getFactory.createGenerator(out) - def parseJsValue(data: Array[Byte]): JsValue = - mapper().readValue(mapper().getFactory.createParser(data), classOf[JsValue]) + mapper().readValue(mapper().createParser(data), classOf[JsValue]) def parseJsValue(input: String): JsValue = - mapper().readValue(mapper().getFactory.createParser(input), classOf[JsValue]) + mapper().readValue(mapper().createParser(input), classOf[JsValue]) def parseJsValue(stream: InputStream): JsValue = - mapper().readValue(mapper().getFactory.createParser(stream), classOf[JsValue]) + mapper().readValue(mapper().createParser(stream), classOf[JsValue]) private def withStringWriter[T](f: StringWriter => T): T = { val sw = new StringWriter() @@ -347,35 +349,29 @@ private[play] case class JacksonJson(defaultMapperJsonConfig: JsonConfig) { def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String = withStringWriter { sw => - val gen = stringJsonGenerator(sw) - - if (escapeNonASCII) { - gen.enable(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature) + val mapperWithEscapeNonASCII = if (escapeNonASCII) { + mapper().rebuild().enable(JsonWriteFeature.ESCAPE_NON_ASCII).build() + } else { + mapper() } - mapper().writeValue(gen, jsValue) + mapperWithEscapeNonASCII.writeValue(sw, jsValue) sw.flush() sw.getBuffer.toString } def prettyPrint(jsValue: JsValue): String = withStringWriter { sw => - val gen = stringJsonGenerator(sw).setPrettyPrinter( - new DefaultPrettyPrinter() - ) val writer: ObjectWriter = mapper().writerWithDefaultPrettyPrinter() - writer.writeValue(gen, jsValue) + writer.writeValue(sw, jsValue) sw.flush() sw.getBuffer.toString } def prettyPrintToStream(jsValue: JsValue, stream: OutputStream): Unit = { - val gen = stringJsonGenerator(stream).setPrettyPrinter( - new DefaultPrettyPrinter() - ) val writer: ObjectWriter = mapper().writerWithDefaultPrettyPrinter() - writer.writeValue(gen, jsValue) + writer.writeValue(stream, jsValue) } def jsValueToBytes(jsValue: JsValue): Array[Byte] = diff --git a/play-json/jvm/src/test/scala/play/api/libs/json/JsonConfigSpec.scala b/play-json/jvm/src/test/scala/play/api/libs/json/JsonConfigSpec.scala index cfd0c407..3415dfff 100644 --- a/play-json/jvm/src/test/scala/play/api/libs/json/JsonConfigSpec.scala +++ b/play-json/jvm/src/test/scala/play/api/libs/json/JsonConfigSpec.scala @@ -4,7 +4,7 @@ package play.api.libs.json -import com.fasterxml.jackson.core.{ StreamReadConstraints, StreamWriteConstraints } +import tools.jackson.core.{ StreamReadConstraints, StreamWriteConstraints } import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala b/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala index c57e5dac..74f1ce26 100644 --- a/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala +++ b/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala @@ -4,18 +4,18 @@ package play.api.libs.json -import com.fasterxml.jackson.core.exc.StreamConstraintsException +import tools.jackson.core.exc.StreamConstraintsException import java.math.BigInteger import java.util.Calendar import java.util.Date import java.util.TimeZone - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper +import tools.jackson.databind.JsonNode +import tools.jackson.databind.ObjectMapper import play.api.libs.functional.syntax._ import play.api.libs.json.Json._ import play.api.libs.json.jackson.JacksonJson +import tools.jackson.databind.node.{ ArrayNode, BigIntegerNode, DoubleNode, NumericNode, ObjectNode } class JsonSpec extends org.specs2.mutable.Specification { @@ -469,36 +469,47 @@ class JsonSpec extends org.specs2.mutable.Specification { } "Serialize and deserialize Jackson ObjectNodes" in { - val on = mapper + val on: ObjectNode = mapper .createObjectNode() .put("foo", 1) .put("bar", "two") - val json = Json.obj("foo" -> 1, "bar" -> "two") - + val json = Json.obj("foo" -> 1, "bar" -> "two") + val deserialized: JsResult[JsonNode] = fromJson[JsonNode](json) toJson(on).must_==(json) and ( - fromJson[JsonNode](json).map(_.toString).must_==(JsSuccess(on.toString)) + deserialized.map(_.isInstanceOf[ObjectNode]).must_==(JsSuccess(true)) + ) and ( + deserialized.map(_.toString).must_==(JsSuccess(on.toString)) ) } "Serialize and deserialize Jackson ArrayNodes" in { - val an = mapper + val an: ArrayNode = mapper .createArrayNode() .add("one") .add(2) - val json = Json.arr("one", 2) + val json = Json.arr("one", 2) + val deserialized: JsResult[JsonNode] = fromJson[JsonNode](json) toJson(an).must(equalTo(json)) and ( - fromJson[JsonNode](json).map(_.toString).must_==(JsSuccess(an.toString)) + deserialized.map(_.isInstanceOf[ArrayNode]).must_==(JsSuccess(true)) + ) and ( + deserialized.map(_.toString).must_==(JsSuccess(an.toString)) ) } "Deserialize integer JsNumber as Jackson number node" in { - val jsNum = JsNumber(new java.math.BigDecimal("50")) - fromJson[JsonNode](jsNum).map(_.toString).must_==(JsSuccess("50")) + val jsNum = JsNumber(new java.math.BigDecimal("50")) + val deserialized: JsResult[JsonNode] = fromJson[JsonNode](jsNum) + deserialized.map(_.isInstanceOf[NumericNode]).must_==(JsSuccess(true)) and ( + deserialized.map(_.toString).must_==(JsSuccess("50")) + ) } "Deserialize float JsNumber as Jackson number node" in { - val jsNum = JsNumber(new java.math.BigDecimal("12.345")) - fromJson[JsonNode](jsNum).map(_.toString).must_==(JsSuccess("12.345")) + val jsNum = JsNumber(new java.math.BigDecimal("12.345")) + val deserialized: JsResult[JsonNode] = fromJson[JsonNode](jsNum) + deserialized.map(_.isInstanceOf[NumericNode]).must_==(JsSuccess(true)) and ( + deserialized.map(_.toString).must_==(JsSuccess("12.345")) + ) } "Serialize JsNumbers with integers correctly" in { @@ -587,7 +598,7 @@ class JsonSpec extends org.specs2.mutable.Specification { "allow parsing objects nested up to max depth" in { try { - val depth = 1000 + val depth = 500 Json.parse(("{\"obj\":" * depth) + "1" + ("}" * depth)) } catch { case _: StackOverflowError => @@ -599,13 +610,13 @@ class JsonSpec extends org.specs2.mutable.Specification { } "disallow parsing nested objects exceeding max depth" in { - val depth = 1001 + val depth = 501 Json .parse(("{\"obj\":" * depth) + "1" + ("}" * depth)) .must(throwA[StreamConstraintsException].like { case e: StreamConstraintsException => e.getMessage.must( equalTo( - "Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamReadConstraints.getMaxNestingDepth()`)" + "Document nesting depth (501) exceeds the maximum allowed (500, from `StreamReadConstraints.getMaxNestingDepth()`)" ) ) }) @@ -613,7 +624,7 @@ class JsonSpec extends org.specs2.mutable.Specification { "allow parsing heavily nested mixed arrays and objects" in { try { - val depth = 1000 - 2 // in the string two open objects { are hardcoded + val depth = 500 - 2 // in the string two open objects { are hardcoded Json.parse("{\"foo\": {\"arr\":" + ("[" * depth) + "1" + ("]" * depth) + "}}") } catch { case _: StackOverflowError => @@ -625,13 +636,13 @@ class JsonSpec extends org.specs2.mutable.Specification { } "disallow parsing heavily nested mixed arrays and objects" in { - val depth = 1001 - 2 // in the string two open objects { are hardcoded + val depth = 501 - 2 // in the string two open objects { are hardcoded Json .parse("{\"foo\": {\"arr\":" + ("[" * depth) + "1" + ("]" * depth) + "}}") .must(throwA[StreamConstraintsException].like { case e: StreamConstraintsException => e.getMessage.must( equalTo( - "Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamReadConstraints.getMaxNestingDepth()`)" + "Document nesting depth (501) exceeds the maximum allowed (500, from `StreamReadConstraints.getMaxNestingDepth()`)" ) ) })