diff --git a/build.sbt b/build.sbt index 675d7e2bc..7c99aacda 100644 --- a/build.sbt +++ b/build.sbt @@ -19,19 +19,12 @@ def specs2(scalaVersion: String) = ("org.specs2" %% s"specs2-$n" % "4.23.0") % Test } -val jacksonDatabindVersion = "2.22.4" -val jacksonDatabind = Seq( - "com.fasterxml.jackson.core" % "jackson-databind" % jacksonDatabindVersion -) - -val jacksonVersion = jacksonDatabindVersion +val jacksonVersion = "3.2.4" 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.15.0" @@ -58,7 +51,11 @@ def playJsonMimaSettings = Seq( case _ => true } - Seq(missingMethodInOld) + Seq( + missingMethodInOld, + // Jackson 3 upgrade: SimpleModule moved from com.fasterxml to tools.jackson + ProblemFilters.exclude[MissingTypesProblem]("play.api.libs.json.jackson.PlayJsonMapperModule"), + ) } ) 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 2ad027cb3..bd0983293 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 @@ -29,8 +29,8 @@ import scala.concurrent.duration.{ Duration, FiniteDuration } import scala.util.control.NonFatal -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.{ ArrayNode, ObjectNode } +import tools.jackson.databind.JsonNode +import tools.jackson.databind.node.{ ArrayNode, 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 e8b24b5b7..57a45ab48 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 @@ -26,7 +26,7 @@ import scala.collection.mutable.{ ArrayBuilder, Map => MMap } import scala.concurrent.duration.FiniteDuration -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 38f999c90..162e8ef27 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 1114a590c..909f870c0 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 @@ -13,26 +13,25 @@ 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.StreamWriteFeature -import com.fasterxml.jackson.core.Version -import com.fasterxml.jackson.core.json.JsonWriteFeature - -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 +import tools.jackson.core.JsonParser +import tools.jackson.core.JsonTokenId +import tools.jackson.core.StreamWriteFeature +import tools.jackson.core.Version +import tools.jackson.core.json.JsonFactory +import tools.jackson.core.json.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.module.scala.DefaultScalaModule import play.api.libs.json._ @@ -65,7 +64,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 = { @@ -78,7 +77,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,17 +88,7 @@ private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSer 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) - } + json.writeNumber(if (shouldWritePlain) new JBigDec(stripped.toPlainString) else stripped) } case JsString(v) => json.writeString(v) @@ -108,7 +97,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() } @@ -116,8 +105,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() } @@ -151,7 +140,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 = { @@ -167,7 +156,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) @@ -192,14 +181,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) @@ -217,7 +206,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") @@ -247,26 +236,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]] } } @@ -282,34 +284,32 @@ 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 } 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 cfd0c4078..3415dfffb 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 ff33544b9..09336d336 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,14 +4,15 @@ package play.api.libs.json -import com.fasterxml.jackson.core.exc.StreamConstraintsException +import tools.jackson.core.exc.StreamConstraintsException import java.io.ByteArrayInputStream import java.math.BigInteger import java.util.{ Calendar, Date, TimeZone } -import com.fasterxml.jackson.databind.{ JsonNode, ObjectMapper } -import com.fasterxml.jackson.databind.node.{ ArrayNode, NumericNode, ObjectNode } +import tools.jackson.databind.{ JsonNode, ObjectMapper } +import tools.jackson.databind.json.JsonMapper +import tools.jackson.databind.node.{ ArrayNode, BigIntegerNode, DoubleNode, NumericNode, ObjectNode } import play.api.libs.functional.syntax._ import play.api.libs.json.Json._ @@ -527,12 +528,11 @@ final class JsonSpec extends org.specs2.mutable.Specification { ) } - "Use a custom ObjectMapper subclass for ASCII serialization" in { + "Use a custom JsonMapper for ASCII serialization" in { val jacksonJson = JacksonJson(JsonConfig.settings) - val customMapper = new ObjectMapper() {} + val customMapper = JsonMapper.builder().addModule(new PlayJsonMapperModule(JsonConfig.settings)).build() jacksonJson.setObjectMapper(customMapper) - customMapper.registerModule(new PlayJsonMapperModule()) jacksonJson .generateFromJsValue(JsString("é"), escapeNonASCII = true) @@ -638,7 +638,7 @@ final 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 => @@ -650,13 +650,13 @@ final 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()`)" ) ) }) @@ -664,7 +664,7 @@ final 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 => @@ -676,13 +676,13 @@ final 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()`)" ) ) })