From 892ad8c7eab05778e6fe8f3ba317b18a2f664b9d Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 22 Nov 2022 14:36:22 -0800 Subject: [PATCH 01/19] Add option to preserve zero decimals --- .../scala/play/api/libs/json/EnvReads.scala | 6 +-- .../scala/play/api/libs/json/EnvWrites.scala | 4 +- .../api/libs/json/JsonParserSettings.scala | 32 +++++++++--- .../play/api/libs/json/StaticBinding.scala | 12 ++--- .../api/libs/json/jackson/JacksonJson.scala | 23 ++++++++- .../scala/play/api/libs/json/JsonSpec.scala | 49 +++++++++++++++++++ 6 files changed, 105 insertions(+), 21 deletions(-) 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 7e5c592b0..9a673c8e6 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 @@ -40,7 +40,7 @@ trait EnvReads { */ implicit object JsonNodeReads extends Reads[JsonNode] { def reads(json: JsValue): JsResult[JsonNode] = - JsSuccess(JacksonJson.jsValueToJsonNode(json)) + JsSuccess(JacksonJson.instance.jsValueToJsonNode(json)) } /** @@ -48,7 +48,7 @@ trait EnvReads { */ implicit object ObjectNodeReads extends Reads[ObjectNode] { def reads(json: JsValue): JsResult[ObjectNode] = { - json.validate[JsObject].map(jo => JacksonJson.jsValueToJsonNode(jo).asInstanceOf[ObjectNode]) + json.validate[JsObject].map(jo => JacksonJson.instance.jsValueToJsonNode(jo).asInstanceOf[ObjectNode]) } } @@ -57,7 +57,7 @@ trait EnvReads { */ implicit object ArrayNodeReads extends Reads[ArrayNode] { def reads(json: JsValue): JsResult[ArrayNode] = { - json.validate[JsArray].map(ja => JacksonJson.jsValueToJsonNode(ja).asInstanceOf[ArrayNode]) + json.validate[JsArray].map(ja => JacksonJson.instance.jsValueToJsonNode(ja).asInstanceOf[ArrayNode]) } } 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 92d21f1eb..502e4066e 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,14 +26,14 @@ trait EnvWrites { @deprecated("Use `jsonNodeWrites`", "2.8.0") object JsonNodeWrites extends Writes[JsonNode] { - def writes(o: JsonNode): JsValue = JacksonJson.jsonNodeToJsValue(o) + def writes(o: JsonNode): JsValue = JacksonJson.instance.jsonNodeToJsValue(o) } /** * Serializer for Jackson JsonNode */ implicit def jsonNodeWrites[T <: JsonNode]: Writes[T] = - Writes[T](JacksonJson.jsonNodeToJsValue) + Writes[T](JacksonJson.instance.jsonNodeToJsValue) /** Typeclass to implement way of formatting of Java8 temporal types. */ trait TemporalFormatter[T <: Temporal] { diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala index 28b5b6aa3..2ee4c756c 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala @@ -4,8 +4,13 @@ package play.api.libs.json -import java.math.MathContext +import play.api.libs.json.JsonParserSettings.MaxPlain +import play.api.libs.json.JsonParserSettings.MinPlain +import play.api.libs.json.JsonParserSettings.defaultDigitsLimit +import play.api.libs.json.JsonParserSettings.defaultPreserveZeroDecimal +import play.api.libs.json.JsonParserSettings.defaultScaleLimit +import java.math.MathContext import scala.util.control.NonFatal /** @@ -18,13 +23,14 @@ import scala.util.control.NonFatal */ final case class BigDecimalParseSettings( mathContext: MathContext = MathContext.DECIMAL128, - scaleLimit: Int, - digitsLimit: Int + scaleLimit: Int = defaultScaleLimit, + digitsLimit: Int = defaultDigitsLimit ) final case class BigDecimalSerializerSettings( - minPlain: BigDecimal, - maxPlain: BigDecimal + minPlain: BigDecimal = MinPlain, + maxPlain: BigDecimal = MaxPlain, + preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal ) final case class JsonParserSettings( @@ -44,6 +50,9 @@ object JsonParserSettings { // Doubles max value has 309 digits, so we are using 310 here val defaultDigitsLimit: Int = 310 + // Drop zero decimal by default. + val defaultPreserveZeroDecimal: Boolean = false + // Maximum magnitude of BigDecimal to write out as a plain string val MaxPlain: BigDecimal = 1E20 @@ -52,11 +61,15 @@ object JsonParserSettings { def apply(): JsonParserSettings = JsonParserSettings( BigDecimalParseSettings(defaultMathContext, defaultScaleLimit, defaultDigitsLimit), - BigDecimalSerializerSettings(minPlain = MinPlain, maxPlain = MaxPlain) + BigDecimalSerializerSettings( + minPlain = MinPlain, + maxPlain = MaxPlain, + preserveZeroDecimal = defaultPreserveZeroDecimal + ) ) /** - * Return the parse settings that are configured. + * Return the default settings that are configured from System properties. */ val settings: JsonParserSettings = { // Initialize the parser settings from System properties. This way it is possible to users @@ -67,6 +80,8 @@ object JsonParserSettings { val minPlain: BigDecimal = parseNum("play.json.serializer.minPlain", MinPlain)(BigDecimal.exact) val maxPlain: BigDecimal = parseNum("play.json.serializer.maxPlain", MaxPlain)(BigDecimal.exact) + val preserveZeroDecimal: Boolean = + parseNum("play.json.serializer.preserveZeroDecimal", defaultPreserveZeroDecimal)(_.toBoolean) JsonParserSettings( BigDecimalParseSettings( @@ -76,7 +91,8 @@ object JsonParserSettings { ), BigDecimalSerializerSettings( minPlain, - maxPlain + maxPlain, + preserveZeroDecimal ) ) } diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala b/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala index 4b77f6268..538e91a2e 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala @@ -10,21 +10,21 @@ object StaticBinding { /** Parses a [[JsValue]] from raw data. */ def parseJsValue(data: Array[Byte]): JsValue = - JacksonJson.parseJsValue(data) + JacksonJson.instance.parseJsValue(data) /** Parses a [[JsValue]] from a string content. */ def parseJsValue(input: String): JsValue = - JacksonJson.parseJsValue(input) + JacksonJson.instance.parseJsValue(input) /** Parses a [[JsValue]] from a stream. */ def parseJsValue(stream: java.io.InputStream): JsValue = - JacksonJson.parseJsValue(stream) + JacksonJson.instance.parseJsValue(stream) def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String = - JacksonJson.generateFromJsValue(jsValue, escapeNonASCII) + JacksonJson.instance.generateFromJsValue(jsValue, escapeNonASCII) - def prettyPrint(jsValue: JsValue): String = JacksonJson.prettyPrint(jsValue) + def prettyPrint(jsValue: JsValue): String = JacksonJson.instance.prettyPrint(jsValue) def toBytes(jsValue: JsValue): Array[Byte] = - JacksonJson.jsValueToBytes(jsValue) + JacksonJson.instance.jsValueToBytes(jsValue) } 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 91384fb34..38c202887 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 @@ -69,6 +69,16 @@ private[jackson] class JsValueSerializer(parserSettings: JsonParserSettings) ext import com.fasterxml.jackson.databind.node.BigIntegerNode import com.fasterxml.jackson.databind.node.DecimalNode + private def stripTrailingZeros(bigDec: JBigDec): JBigDec = { + val stripped = bigDec.stripTrailingZeros + if (parserSettings.bigDecimalSerializerSettings.preserveZeroDecimal && bigDec.scale > 0 && stripped.scale == 0) { + // restore .0 if rounded to a whole number + stripped.setScale(1) + } else { + stripped + } + } + override def serialize(value: JsValue, json: JsonGenerator, provider: SerializerProvider): Unit = { value match { case JsNumber(v) => { @@ -79,7 +89,7 @@ private[jackson] class JsValueSerializer(parserSettings: JsonParserSettings) ext val va = v.abs va < parserSettings.bigDecimalSerializerSettings.maxPlain && va > parserSettings.bigDecimalSerializerSettings.minPlain } - val stripped = v.bigDecimal.stripTrailingZeros + val stripped = stripTrailingZeros(v.bigDecimal) val raw = if (shouldWritePlain) stripped.toPlainString else stripped.toString if (raw.indexOf('E') < 0 && raw.indexOf('.') < 0) @@ -257,7 +267,16 @@ private[jackson] class PlaySerializers(parserSettings: JsonParserSettings) exten } private[json] object JacksonJson { - private lazy val mapper = (new ObjectMapper).registerModule(new PlayJsonModule(JsonParserSettings.settings)) + + /** + * Instance used to serialize and deserialize JSON. This is configured with system properties, but can be + * overridden for testing. + */ + var instance: JacksonJson = JacksonJson(JsonParserSettings.settings) +} + +private[json] case class JacksonJson(settings: JsonParserSettings) { + private lazy val mapper = (new ObjectMapper).registerModule(new PlayJsonModule(settings)) private lazy val jsonFactory = new JsonFactory(mapper) 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 bc3813c0c..9dc950a24 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 @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import play.api.libs.functional.syntax._ import play.api.libs.json.Json._ +import play.api.libs.json.jackson.JacksonJson class JsonSpec extends org.specs2.mutable.Specification { "JSON".title @@ -69,6 +70,24 @@ class JsonSpec extends org.specs2.mutable.Specification { val mapper = new ObjectMapper() + val preserveZeroDecimal: JacksonJson = { + val defaultSerializerSettings = JsonParserSettings.settings.bigDecimalSerializerSettings + val defaultParserSettings = JsonParserSettings.settings.bigDecimalParseSettings + val serializerSettings = BigDecimalSerializerSettings( + defaultSerializerSettings.minPlain, defaultSerializerSettings.maxPlain, preserveZeroDecimal = true) + JacksonJson(JsonParserSettings(defaultParserSettings, serializerSettings)) + } + + def withJacksonJson[T](jacksonJson: JacksonJson)(f: () => T) = { + val oldInstance = JacksonJson.instance + try { + JacksonJson.instance = jacksonJson + f.apply() + } finally { + JacksonJson.instance = oldInstance + } + } + "Complete JSON should create full object" >> { lazy val postDate: Date = dateParser.parse("2011-04-22T13:33:48Z") @@ -278,6 +297,36 @@ class JsonSpec extends org.specs2.mutable.Specification { numbers.bigDec.mustEqual(BigDecimal("10.12345678901234567890123456789012")) } + "drop trailing zeros for non-zero decimal by default" in { + val s = stringify(toJson(BigDecimal("1.020300"))) + s.mustEqual("1.0203") + } + + "drop single trailing zero decimal by default" in { + val s = stringify(toJson(BigDecimal("1.0"))) + s.mustEqual("1") + } + + "drop multiple trailing zero decimals by default" in { + val s = stringify(toJson(BigDecimal("1.00"))) + s.mustEqual("1") + } + + "drop multiple trailing zeros for non-zero decimal with preserveZeroDecimal=true" in { + val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.020300")))) + s.mustEqual("1.0203") + } + + "do not drop single trailing zero decimal with preserveZeroDecimal=true" in { + val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.0")))) + s.mustEqual("1.0") + } + + "preserve a single trailing zero decimal with preserveZeroDecimal=true" in { + val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.00")))) + s.mustEqual("1.0") + } + "success when not exceeding the scale limit for positive numbers" in { val withinScaleLimit = BigDecimal(2, parserSettings.bigDecimalParseSettings.scaleLimit - 1) Json From 19178578212df988e8c8f1b0f0ba67ba4d677c35 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 22 Nov 2022 15:41:19 -0800 Subject: [PATCH 02/19] scalafmt --- .../jvm/src/test/scala/play/api/libs/json/JsonSpec.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 9dc950a24..2c66348e4 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 @@ -72,9 +72,12 @@ class JsonSpec extends org.specs2.mutable.Specification { val preserveZeroDecimal: JacksonJson = { val defaultSerializerSettings = JsonParserSettings.settings.bigDecimalSerializerSettings - val defaultParserSettings = JsonParserSettings.settings.bigDecimalParseSettings + val defaultParserSettings = JsonParserSettings.settings.bigDecimalParseSettings val serializerSettings = BigDecimalSerializerSettings( - defaultSerializerSettings.minPlain, defaultSerializerSettings.maxPlain, preserveZeroDecimal = true) + defaultSerializerSettings.minPlain, + defaultSerializerSettings.maxPlain, + preserveZeroDecimal = true + ) JacksonJson(JsonParserSettings(defaultParserSettings, serializerSettings)) } From 1694da9e08c04cd39e2c08191c06bc034463f256 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 22 Nov 2022 15:45:57 -0800 Subject: [PATCH 03/19] JDK 11 binary compatibility check --- .../main/scala/play/api/libs/json/JsonParserSettings.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala index 2ee4c756c..8dfb30d8c 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala @@ -33,6 +33,11 @@ final case class BigDecimalSerializerSettings( preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal ) +object BigDecimalSerializerSettings { + def apply(minPlain: BigDecimal, maxPlain: BigDecimal): BigDecimalSerializerSettings = + BigDecimalSerializerSettings(minPlain, maxPlain) +} + final case class JsonParserSettings( bigDecimalParseSettings: BigDecimalParseSettings, bigDecimalSerializerSettings: BigDecimalSerializerSettings From bfdf86c6b1c3c9dc6cfbf4350929aee47b62b1bf Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 22 Nov 2022 15:48:52 -0800 Subject: [PATCH 04/19] revert last commit --- .../main/scala/play/api/libs/json/JsonParserSettings.scala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala index 8dfb30d8c..2ee4c756c 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala @@ -33,11 +33,6 @@ final case class BigDecimalSerializerSettings( preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal ) -object BigDecimalSerializerSettings { - def apply(minPlain: BigDecimal, maxPlain: BigDecimal): BigDecimalSerializerSettings = - BigDecimalSerializerSettings(minPlain, maxPlain) -} - final case class JsonParserSettings( bigDecimalParseSettings: BigDecimalParseSettings, bigDecimalSerializerSettings: BigDecimalSerializerSettings From 8b17d3277190618e1e90e6dafbb883a7ce2b7f94 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 22 Nov 2022 16:10:32 -0800 Subject: [PATCH 05/19] fix binary compatibility, add tests for zero --- .../scala/play/api/libs/json/JsonParserSettings.scala | 9 +++++---- .../src/test/scala/play/api/libs/json/JsonSpec.scala | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala index 2ee4c756c..8cf8e8918 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala @@ -11,6 +11,7 @@ import play.api.libs.json.JsonParserSettings.defaultPreserveZeroDecimal import play.api.libs.json.JsonParserSettings.defaultScaleLimit import java.math.MathContext + import scala.util.control.NonFatal /** @@ -23,13 +24,13 @@ import scala.util.control.NonFatal */ final case class BigDecimalParseSettings( mathContext: MathContext = MathContext.DECIMAL128, - scaleLimit: Int = defaultScaleLimit, - digitsLimit: Int = defaultDigitsLimit + scaleLimit: Int, + digitsLimit: Int ) final case class BigDecimalSerializerSettings( - minPlain: BigDecimal = MinPlain, - maxPlain: BigDecimal = MaxPlain, + minPlain: BigDecimal, + maxPlain: BigDecimal, preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal ) 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 2c66348e4..36de3dc5a 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 @@ -315,6 +315,11 @@ class JsonSpec extends org.specs2.mutable.Specification { s.mustEqual("1") } + "drop multiple trailing zero decimals from zero value by default" in { + val s = stringify(toJson(BigDecimal("0.00"))) + s.mustEqual("0") + } + "drop multiple trailing zeros for non-zero decimal with preserveZeroDecimal=true" in { val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.020300")))) s.mustEqual("1.0203") @@ -330,6 +335,11 @@ class JsonSpec extends org.specs2.mutable.Specification { s.mustEqual("1.0") } + "preserve a single trailing zero decimal from zero value with preserveZeroDecimal=true" in { + val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0.00")))) + s.mustEqual("0.0") + } + "success when not exceeding the scale limit for positive numbers" in { val withinScaleLimit = BigDecimal(2, parserSettings.bigDecimalParseSettings.scaleLimit - 1) Json From 6f3c1768fbc5475255809c9df774e6993f538d41 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 22 Nov 2022 16:21:13 -0800 Subject: [PATCH 06/19] cleanup imports, use atomic ref --- .../main/scala/play/api/libs/json/EnvReads.scala | 6 +++--- .../main/scala/play/api/libs/json/EnvWrites.scala | 4 ++-- .../play/api/libs/json/JsonParserSettings.scala | 4 ---- .../scala/play/api/libs/json/StaticBinding.scala | 12 ++++++------ .../play/api/libs/json/jackson/JacksonJson.scala | 14 +++++++++----- .../test/scala/play/api/libs/json/JsonSpec.scala | 6 +++--- 6 files changed, 23 insertions(+), 23 deletions(-) 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 9a673c8e6..f73d041ec 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 @@ -40,7 +40,7 @@ trait EnvReads { */ implicit object JsonNodeReads extends Reads[JsonNode] { def reads(json: JsValue): JsResult[JsonNode] = - JsSuccess(JacksonJson.instance.jsValueToJsonNode(json)) + JsSuccess(JacksonJson.get.jsValueToJsonNode(json)) } /** @@ -48,7 +48,7 @@ trait EnvReads { */ implicit object ObjectNodeReads extends Reads[ObjectNode] { def reads(json: JsValue): JsResult[ObjectNode] = { - json.validate[JsObject].map(jo => JacksonJson.instance.jsValueToJsonNode(jo).asInstanceOf[ObjectNode]) + json.validate[JsObject].map(jo => JacksonJson.get.jsValueToJsonNode(jo).asInstanceOf[ObjectNode]) } } @@ -57,7 +57,7 @@ trait EnvReads { */ implicit object ArrayNodeReads extends Reads[ArrayNode] { def reads(json: JsValue): JsResult[ArrayNode] = { - json.validate[JsArray].map(ja => JacksonJson.instance.jsValueToJsonNode(ja).asInstanceOf[ArrayNode]) + json.validate[JsArray].map(ja => JacksonJson.get.jsValueToJsonNode(ja).asInstanceOf[ArrayNode]) } } 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 502e4066e..cf055eeed 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,14 +26,14 @@ trait EnvWrites { @deprecated("Use `jsonNodeWrites`", "2.8.0") object JsonNodeWrites extends Writes[JsonNode] { - def writes(o: JsonNode): JsValue = JacksonJson.instance.jsonNodeToJsValue(o) + def writes(o: JsonNode): JsValue = JacksonJson.get.jsonNodeToJsValue(o) } /** * Serializer for Jackson JsonNode */ implicit def jsonNodeWrites[T <: JsonNode]: Writes[T] = - Writes[T](JacksonJson.instance.jsonNodeToJsValue) + Writes[T](JacksonJson.get.jsonNodeToJsValue) /** Typeclass to implement way of formatting of Java8 temporal types. */ trait TemporalFormatter[T <: Temporal] { diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala index 8cf8e8918..0366ae53c 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala @@ -4,11 +4,7 @@ package play.api.libs.json -import play.api.libs.json.JsonParserSettings.MaxPlain -import play.api.libs.json.JsonParserSettings.MinPlain -import play.api.libs.json.JsonParserSettings.defaultDigitsLimit import play.api.libs.json.JsonParserSettings.defaultPreserveZeroDecimal -import play.api.libs.json.JsonParserSettings.defaultScaleLimit import java.math.MathContext diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala b/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala index 538e91a2e..e7f122c2c 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala @@ -10,21 +10,21 @@ object StaticBinding { /** Parses a [[JsValue]] from raw data. */ def parseJsValue(data: Array[Byte]): JsValue = - JacksonJson.instance.parseJsValue(data) + JacksonJson.get.parseJsValue(data) /** Parses a [[JsValue]] from a string content. */ def parseJsValue(input: String): JsValue = - JacksonJson.instance.parseJsValue(input) + JacksonJson.get.parseJsValue(input) /** Parses a [[JsValue]] from a stream. */ def parseJsValue(stream: java.io.InputStream): JsValue = - JacksonJson.instance.parseJsValue(stream) + JacksonJson.get.parseJsValue(stream) def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String = - JacksonJson.instance.generateFromJsValue(jsValue, escapeNonASCII) + JacksonJson.get.generateFromJsValue(jsValue, escapeNonASCII) - def prettyPrint(jsValue: JsValue): String = JacksonJson.instance.prettyPrint(jsValue) + def prettyPrint(jsValue: JsValue): String = JacksonJson.get.prettyPrint(jsValue) def toBytes(jsValue: JsValue): Array[Byte] = - JacksonJson.instance.jsValueToBytes(jsValue) + JacksonJson.get.jsValueToBytes(jsValue) } 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 38c202887..1f0a6afff 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 @@ -6,13 +6,11 @@ package play.api.libs.json.jackson import java.io.InputStream import java.io.StringWriter - import scala.annotation.switch import scala.annotation.tailrec import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ListBuffer - import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser @@ -20,16 +18,16 @@ 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.module.SimpleModule import com.fasterxml.jackson.databind.ser.Serializers - import play.api.libs.json._ +import java.util.concurrent.atomic.AtomicReference + /** * The Play JSON module for Jackson. * @@ -267,12 +265,18 @@ private[jackson] class PlaySerializers(parserSettings: JsonParserSettings) exten } private[json] object JacksonJson { + val defaultInstance: JacksonJson = JacksonJson(JsonParserSettings.settings) + + private val ref: AtomicReference[JacksonJson] = new AtomicReference[JacksonJson](defaultInstance) /** * Instance used to serialize and deserialize JSON. This is configured with system properties, but can be * overridden for testing. */ - var instance: JacksonJson = JacksonJson(JsonParserSettings.settings) + var get: JacksonJson = ref.get + + /** Sets the instance for testing and returns the old value. */ + def set(instance: JacksonJson): JacksonJson = ref.getAndSet(instance) } private[json] case class JacksonJson(settings: JsonParserSettings) { 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 36de3dc5a..1f4c745f2 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 @@ -82,12 +82,12 @@ class JsonSpec extends org.specs2.mutable.Specification { } def withJacksonJson[T](jacksonJson: JacksonJson)(f: () => T) = { - val oldInstance = JacksonJson.instance + val oldInstance = JacksonJson.set(jacksonJson) try { - JacksonJson.instance = jacksonJson + JacksonJson.get = jacksonJson f.apply() } finally { - JacksonJson.instance = oldInstance + JacksonJson.set(oldInstance) } } From 2a9615eaf26b74324ccf5edca518e1701d0a410c Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 22 Nov 2022 16:26:58 -0800 Subject: [PATCH 07/19] remove import --- .../src/main/scala/play/api/libs/json/JsonParserSettings.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala index 0366ae53c..c68f63a37 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala @@ -4,8 +4,6 @@ package play.api.libs.json -import play.api.libs.json.JsonParserSettings.defaultPreserveZeroDecimal - import java.math.MathContext import scala.util.control.NonFatal From 8840aaedaaf54a26a2cd265db13ec6801fbb87c5 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 22 Nov 2022 16:31:19 -0800 Subject: [PATCH 08/19] restore import --- .../src/main/scala/play/api/libs/json/JsonParserSettings.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala index c68f63a37..0f86e993a 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala @@ -4,8 +4,9 @@ package play.api.libs.json -import java.math.MathContext +import play.api.libs.json.JsonParserSettings.defaultPreserveZeroDecimal +import java.math.MathContext import scala.util.control.NonFatal /** From 932f02ded5a4c24cef11595975e61dc8cad4d30c Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 29 Nov 2022 11:20:48 -0800 Subject: [PATCH 09/19] fix binary compatibility, formatting --- .../play/api/libs/json/BigDecimalParser.scala | 8 +- .../scala/play/api/libs/json/JsonConfig.scala | 207 ++++++++++++++++++ .../api/libs/json/JsonParserSettings.scala | 110 ---------- .../api/libs/json/jackson/JacksonJson.scala | 50 +++-- .../scala/play/api/libs/json/JsonSpec.scala | 11 +- 5 files changed, 246 insertions(+), 140 deletions(-) create mode 100644 play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala delete mode 100644 play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/BigDecimalParser.scala b/play-json/jvm/src/main/scala/play/api/libs/json/BigDecimalParser.scala index 78c2738dd..033eb7e72 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/BigDecimalParser.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/BigDecimalParser.scala @@ -6,21 +6,21 @@ package play.api.libs.json private[json] object BigDecimalParser { - def parse(input: String, settings: JsonParserSettings): JsResult[java.math.BigDecimal] = { + def parse(input: String, jsonConfig: JsonConfig): JsResult[java.math.BigDecimal] = { // There is a limit of how large the numbers can be since parsing extremely // large numbers (think thousand of digits) and operating on the parsed values // can potentially cause a DDoS. - if (input.length > settings.bigDecimalParseSettings.digitsLimit) { + if (input.length > jsonConfig.bigDecimalParseConfig.digitsLimit) { JsError("error.expected.numberdigitlimit") } else { // Must create the BigDecimal with a MathContext that is consistent with the limits used. try { - val bigDecimal = new java.math.BigDecimal(input, settings.bigDecimalParseSettings.mathContext) + val bigDecimal = new java.math.BigDecimal(input, jsonConfig.bigDecimalParseConfig.mathContext) // We should also avoid numbers with scale that are out of a safe limit val scale = bigDecimal.scale - if (Math.abs(scale) > settings.bigDecimalParseSettings.scaleLimit) { + if (Math.abs(scale) > jsonConfig.bigDecimalParseConfig.scaleLimit) { JsError(JsonValidationError("error.expected.numberscalelimit", scale)) } else { JsSuccess(bigDecimal) 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 new file mode 100644 index 000000000..81ad713c8 --- /dev/null +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import play.api.libs.json.JsonConfig.MaxPlain +import play.api.libs.json.JsonConfig.MinPlain +import play.api.libs.json.JsonConfig.defaultDigitsLimit +import play.api.libs.json.JsonConfig.defaultMathContext +import play.api.libs.json.JsonConfig.defaultPreserveZeroDecimal +import play.api.libs.json.JsonConfig.defaultScaleLimit +import play.api.libs.json.JsonConfig.loadDigitsLimit +import play.api.libs.json.JsonConfig.loadMathContext +import play.api.libs.json.JsonConfig.loadMaxPlain +import play.api.libs.json.JsonConfig.loadMinPlain +import play.api.libs.json.JsonConfig.loadScaleLimit + +import java.math.MathContext + +import scala.util.control.NonFatal + +/** + * Parse settings for BigDecimals. Defines limits that will be used when parsing the BigDecimals, like how many digits + * are accepted. + */ +sealed trait BigDecimalParseConfig { + + /** The [[MathContext]] used when parsing. */ + def mathContext: MathContext + + /** Limits the scale, and it is related to the math context used. */ + def scaleLimit: Int + + /** How many digits are accepted, also related to the math context used. */ + def digitsLimit: Int +} + +object BigDecimalParseConfig { + def apply( + mathContext: MathContext = defaultMathContext, + scaleLimit: Int = defaultScaleLimit, + digitsLimit: Int = defaultDigitsLimit + ): BigDecimalParseConfig = BigDecimalParseConfigImpl(mathContext, scaleLimit, digitsLimit) +} + +private final case class BigDecimalParseConfigImpl(mathContext: MathContext, scaleLimit: Int, digitsLimit: Int) + extends BigDecimalParseConfig + +sealed trait BigDecimalSerializerConfig { + + /** Minimum magnitude of BigDecimal to write out as a plain string. */ + def minPlain: BigDecimal + + /** Maximum magnitude of BigDecimal to write out as a plain string. */ + def maxPlain: BigDecimal + + /** True to preserve zero decimals (false by default). */ + def preserveZeroDecimal: Boolean +} + +object BigDecimalSerializerConfig { + def apply( + minPlain: BigDecimal = MinPlain, + maxPlain: BigDecimal = MaxPlain, + preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal + ): BigDecimalSerializerConfig = + DecimalSerializerSettingsImpl(minPlain, maxPlain, preserveZeroDecimal) +} + +private final case class DecimalSerializerSettingsImpl( + minPlain: BigDecimal, + maxPlain: BigDecimal, + preserveZeroDecimal: Boolean +) extends BigDecimalSerializerConfig + +sealed trait JsonConfig { + def bigDecimalParseConfig: BigDecimalParseConfig + def bigDecimalSerializerConfig: BigDecimalSerializerConfig +} + +object JsonConfig { + val defaultMathContext: MathContext = MathContext.DECIMAL128 + + // Limit for the scale considering the MathContext of 128 + // limit for scale for decimal128: BigDecimal("0." + "0" * 33 + "1e-6143", java.math.MathContext.DECIMAL128).scale + 1 + val defaultScaleLimit: Int = 6178 + + // 307 digits should be the correct value for 128 bytes. But we are using 310 + // because Play JSON uses BigDecimal to parse any number including Doubles and + // Doubles max value has 309 digits, so we are using 310 here + val defaultDigitsLimit: Int = 310 + + // Drop zero decimal by default. + val defaultPreserveZeroDecimal: Boolean = false + + // Maximum magnitude of BigDecimal to write out as a plain string + val MaxPlain: BigDecimal = 1E20 + + // Minimum magnitude of BigDecimal to write out as a plain string + val MinPlain: BigDecimal = 1E-10 + + private[json] def loadScaleLimit: Int = parseNum("play.json.parser.scaleLimit", defaultScaleLimit)(_.toInt) + private[json] def loadDigitsLimit: Int = parseNum("play.json.parser.digitsLimit", defaultDigitsLimit)(_.toInt) + + private[json] def loadMathContext: MathContext = parseMathContext("play.json.parser.mathContext") + + private[json] def loadMinPlain: BigDecimal = parseNum("play.json.serializer.minPlain", MinPlain)(BigDecimal.exact) + + private[json] def loadMaxPlain: BigDecimal = parseNum("play.json.serializer.maxPlain", MaxPlain)(BigDecimal.exact) + + private[json] def loadPreserveZeroDecimal: Boolean = + parseNum("play.json.serializer.preserveZeroDecimal", defaultPreserveZeroDecimal)(_.toBoolean) + + val settings: JsonConfig = + JsonConfig( + BigDecimalParseConfig(loadMathContext, loadScaleLimit, loadDigitsLimit), + BigDecimalSerializerConfig(loadMinPlain, loadMaxPlain, loadPreserveZeroDecimal) + ) + + def apply(): JsonConfig = apply(BigDecimalParseConfig(), BigDecimalSerializerConfig()) + + def apply( + bigDecimalParseConfig: BigDecimalParseConfig, + bigDecimalSerializerConfig: BigDecimalSerializerConfig + ): JsonConfig = + JsonConfigImpl(bigDecimalParseConfig, bigDecimalSerializerConfig) + + private[json] def parseMathContext(key: String): MathContext = sys.props.get(key).map(_.toLowerCase) match { + case Some("decimal128") => MathContext.DECIMAL128 + case Some("decimal64") => MathContext.DECIMAL64 + case Some("decimal32") => MathContext.DECIMAL32 + case Some("unlimited") => MathContext.UNLIMITED + case _ => defaultMathContext + } + + private[json] def parseNum[T](key: String, default: T)(f: String => T): T = + try { + sys.props.get(key).map(f).getOrElse(default) + } catch { + case NonFatal(_) => default + } +} + +private final case class JsonConfigImpl( + bigDecimalParseConfig: BigDecimalParseConfig, + bigDecimalSerializerConfig: BigDecimalSerializerConfig +) extends JsonConfig + +@deprecated("Use BigDecimalParseConfig instead", "2.10.0") +final case class BigDecimalParseSettings( + mathContext: MathContext = MathContext.DECIMAL128, + scaleLimit: Int, + digitsLimit: Int +) extends BigDecimalParseConfig + +@deprecated("Use BigDecimalSerializerConfig instead", "2.10.0") +final case class BigDecimalSerializerSettings( + minPlain: BigDecimal, + maxPlain: BigDecimal +) extends BigDecimalSerializerConfig { + override def preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal +} + +@deprecated("Use JsonConfig instead", "2.10.0") +final case class JsonParserSettings( + bigDecimalParseSettings: BigDecimalParseSettings, + bigDecimalSerializerSettings: BigDecimalSerializerSettings +) extends JsonConfig { + override def bigDecimalParseConfig: BigDecimalParseConfig = bigDecimalParseSettings + + override def bigDecimalSerializerConfig: BigDecimalSerializerConfig = bigDecimalSerializerSettings +} + +object JsonParserSettings { + val defaultMathContext: MathContext = JsonConfig.defaultMathContext + + // Limit for the scale considering the MathContext of 128 + // limit for scale for decimal128: BigDecimal("0." + "0" * 33 + "1e-6143", java.math.MathContext.DECIMAL128).scale + 1 + val defaultScaleLimit: Int = JsonConfig.defaultScaleLimit + + // 307 digits should be the correct value for 128 bytes. But we are using 310 + // because Play JSON uses BigDecimal to parse any number including Doubles and + // Doubles max value has 309 digits, so we are using 310 here + val defaultDigitsLimit: Int = JsonConfig.defaultDigitsLimit + + // Maximum magnitude of BigDecimal to write out as a plain string + val MaxPlain: BigDecimal = JsonConfig.MaxPlain + + // Minimum magnitude of BigDecimal to write out as a plain string + val MinPlain: BigDecimal = JsonConfig.MinPlain + + def apply(): JsonParserSettings = JsonParserSettings( + BigDecimalParseSettings(defaultMathContext, defaultScaleLimit, defaultDigitsLimit), + BigDecimalSerializerSettings(minPlain = MinPlain, maxPlain = MaxPlain) + ) + + /** + * Return the default settings configured from System properties. + */ + val settings: JsonParserSettings = { + JsonParserSettings( + BigDecimalParseSettings(loadMathContext, loadScaleLimit, loadDigitsLimit), + BigDecimalSerializerSettings(loadMinPlain, loadMaxPlain) + ) + } +} diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala deleted file mode 100644 index 0f86e993a..000000000 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2009-2021 Lightbend Inc. - */ - -package play.api.libs.json - -import play.api.libs.json.JsonParserSettings.defaultPreserveZeroDecimal - -import java.math.MathContext -import scala.util.control.NonFatal - -/** - * Parse settings for BigDecimals. Defines limits that will be used when parsing the BigDecimals, like how many digits - * are accepted. - * - * @param mathContext the [[MathContext]] used when parsing. - * @param scaleLimit limit the scale, and it is related to the math context used. - * @param digitsLimit how many digits are accepted, also related to the math context used. - */ -final case class BigDecimalParseSettings( - mathContext: MathContext = MathContext.DECIMAL128, - scaleLimit: Int, - digitsLimit: Int -) - -final case class BigDecimalSerializerSettings( - minPlain: BigDecimal, - maxPlain: BigDecimal, - preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal -) - -final case class JsonParserSettings( - bigDecimalParseSettings: BigDecimalParseSettings, - bigDecimalSerializerSettings: BigDecimalSerializerSettings -) - -object JsonParserSettings { - val defaultMathContext: MathContext = MathContext.DECIMAL128 - - // Limit for the scale considering the MathContext of 128 - // limit for scale for decimal128: BigDecimal("0." + "0" * 33 + "1e-6143", java.math.MathContext.DECIMAL128).scale + 1 - val defaultScaleLimit: Int = 6178 - - // 307 digits should be the correct value for 128 bytes. But we are using 310 - // because Play JSON uses BigDecimal to parse any number including Doubles and - // Doubles max value has 309 digits, so we are using 310 here - val defaultDigitsLimit: Int = 310 - - // Drop zero decimal by default. - val defaultPreserveZeroDecimal: Boolean = false - - // Maximum magnitude of BigDecimal to write out as a plain string - val MaxPlain: BigDecimal = 1E20 - - // Minimum magnitude of BigDecimal to write out as a plain string - val MinPlain: BigDecimal = 1E-10 - - def apply(): JsonParserSettings = JsonParserSettings( - BigDecimalParseSettings(defaultMathContext, defaultScaleLimit, defaultDigitsLimit), - BigDecimalSerializerSettings( - minPlain = MinPlain, - maxPlain = MaxPlain, - preserveZeroDecimal = defaultPreserveZeroDecimal - ) - ) - - /** - * Return the default settings that are configured from System properties. - */ - val settings: JsonParserSettings = { - // Initialize the parser settings from System properties. This way it is possible to users - // to easily replace the default values. - val scaleLimit = parseNum("play.json.parser.scaleLimit", defaultScaleLimit)(_.toInt) - val digitsLimit = parseNum("play.json.parser.digitsLimit", defaultDigitsLimit)(_.toInt) - val mathContext = parseMathContext("play.json.parser.mathContext") - - val minPlain: BigDecimal = parseNum("play.json.serializer.minPlain", MinPlain)(BigDecimal.exact) - val maxPlain: BigDecimal = parseNum("play.json.serializer.maxPlain", MaxPlain)(BigDecimal.exact) - val preserveZeroDecimal: Boolean = - parseNum("play.json.serializer.preserveZeroDecimal", defaultPreserveZeroDecimal)(_.toBoolean) - - JsonParserSettings( - BigDecimalParseSettings( - mathContext, - scaleLimit, - digitsLimit - ), - BigDecimalSerializerSettings( - minPlain, - maxPlain, - preserveZeroDecimal - ) - ) - } - - private def parseMathContext(key: String): MathContext = sys.props.get(key).map(_.toLowerCase) match { - case Some("decimal128") => MathContext.DECIMAL128 - case Some("decimal64") => MathContext.DECIMAL64 - case Some("decimal32") => MathContext.DECIMAL32 - case Some("unlimited") => MathContext.UNLIMITED - case _ => defaultMathContext - } - - private def parseNum[T](key: String, default: T)(f: String => T): T = - try { - sys.props.get(key).map(f).getOrElse(default) - } catch { - case NonFatal(_) => default - } -} 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 1f0a6afff..4a5e406dd 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 @@ -6,11 +6,13 @@ package play.api.libs.json.jackson import java.io.InputStream import java.io.StringWriter + import scala.annotation.switch import scala.annotation.tailrec import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ListBuffer + import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser @@ -18,12 +20,14 @@ 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.module.SimpleModule import com.fasterxml.jackson.databind.ser.Serializers + import play.api.libs.json._ import java.util.concurrent.atomic.AtomicReference @@ -41,26 +45,30 @@ import java.util.concurrent.atomic.AtomicReference * import play.api.libs.json.jackson.PlayJsonModule * import play.api.libs.json.JsonParserSettings * - * val jsonParseSettings = JsonParserSettings() + * val jsonSettings = JsonSettings.settings * val mapper = new ObjectMapper().registerModule( - * new PlayJsonModule(jsonParseSettings)) + * new PlayJsonMapperModule(jsonSettings)) * val jsValue = mapper.readValue("""{"foo":"bar"}""", classOf[JsValue]) * }}} */ -sealed class PlayJsonModule(parserSettings: JsonParserSettings) - extends SimpleModule("PlayJson", Version.unknownVersion()) { - override def setupModule(context: SetupContext): Unit = { - context.addDeserializers(new PlayDeserializers(parserSettings)) - context.addSerializers(new PlaySerializers(parserSettings)) - } +@deprecated("Use PlayJsonMapperModule class instead", "2.10.0") +sealed class PlayJsonModule(parserSettings: JsonParserSettings) extends PlayJsonMapperModule(parserSettings) { + override def setupModule(context: SetupContext): Unit = super.setupModule(context) } @deprecated("Use PlayJsonModule class instead", "2.6.11") object PlayJsonModule extends PlayJsonModule(JsonParserSettings()) +sealed class PlayJsonMapperModule(jsonConfig: JsonConfig) extends SimpleModule("PlayJson", Version.unknownVersion()) { + override def setupModule(context: SetupContext): Unit = { + context.addDeserializers(new PlayDeserializers(jsonConfig)) + context.addSerializers(new PlaySerializers(jsonConfig)) + } +} + // -- Serializers. -private[jackson] class JsValueSerializer(parserSettings: JsonParserSettings) extends JsonSerializer[JsValue] { +private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSerializer[JsValue] { import java.math.BigInteger import java.math.{ BigDecimal => JBigDec } @@ -69,7 +77,7 @@ private[jackson] class JsValueSerializer(parserSettings: JsonParserSettings) ext private def stripTrailingZeros(bigDec: JBigDec): JBigDec = { val stripped = bigDec.stripTrailingZeros - if (parserSettings.bigDecimalSerializerSettings.preserveZeroDecimal && bigDec.scale > 0 && stripped.scale == 0) { + if (jsonConfig.bigDecimalSerializerConfig.preserveZeroDecimal && bigDec.scale > 0 && stripped.scale == 0) { // restore .0 if rounded to a whole number stripped.setScale(1) } else { @@ -85,7 +93,7 @@ private[jackson] class JsValueSerializer(parserSettings: JsonParserSettings) ext // configuration is ignored when called from ObjectMapper.valueToTree val shouldWritePlain = { val va = v.abs - va < parserSettings.bigDecimalSerializerSettings.maxPlain && va > parserSettings.bigDecimalSerializerSettings.minPlain + va < jsonConfig.bigDecimalSerializerConfig.maxPlain && va > jsonConfig.bigDecimalSerializerConfig.minPlain } val stripped = stripTrailingZeros(v.bigDecimal) val raw = if (shouldWritePlain) stripped.toPlainString else stripped.toString @@ -144,7 +152,7 @@ private[jackson] case class ReadingMap(content: ListBuffer[(String, JsValue)]) e throw new Exception("Cannot add a value on an object without a key, malformed JSON object!") } -private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[_], parserSettings: JsonParserSettings) +private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[_], jsonConfig: JsonConfig) extends JsonDeserializer[Object] { override def isCachable: Boolean = true @@ -161,7 +169,7 @@ private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[_] jp: JsonParser, parserContext: List[DeserializerContext] ): (Some[JsNumber], List[DeserializerContext]) = { - BigDecimalParser.parse(jp.getText, parserSettings) match { + BigDecimalParser.parse(jp.getText, jsonConfig) match { case JsSuccess(bigDecimal, _) => (Some(JsNumber(bigDecimal)), parserContext) @@ -244,19 +252,19 @@ private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[_] override val getNullValue = JsNull } -private[jackson] class PlayDeserializers(parserSettings: JsonParserSettings) extends Deserializers.Base { +private[jackson] class PlayDeserializers(jsonSettings: JsonConfig) extends Deserializers.Base { override def findBeanDeserializer(javaType: JavaType, config: DeserializationConfig, beanDesc: BeanDescription) = { val klass = javaType.getRawClass if (classOf[JsValue].isAssignableFrom(klass) || klass == JsNull.getClass) { - new JsValueDeserializer(config.getTypeFactory, klass, parserSettings) + new JsValueDeserializer(config.getTypeFactory, klass, jsonSettings) } else null } } -private[jackson] class PlaySerializers(parserSettings: JsonParserSettings) extends Serializers.Base { +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)) { - new JsValueSerializer(parserSettings) + new JsValueSerializer(jsonSettings) } else { null } @@ -265,7 +273,7 @@ private[jackson] class PlaySerializers(parserSettings: JsonParserSettings) exten } private[json] object JacksonJson { - val defaultInstance: JacksonJson = JacksonJson(JsonParserSettings.settings) + val defaultInstance: JacksonJson = JacksonJson(JsonConfig.settings) private val ref: AtomicReference[JacksonJson] = new AtomicReference[JacksonJson](defaultInstance) @@ -273,14 +281,14 @@ private[json] object JacksonJson { * Instance used to serialize and deserialize JSON. This is configured with system properties, but can be * overridden for testing. */ - var get: JacksonJson = ref.get + def get: JacksonJson = ref.get /** Sets the instance for testing and returns the old value. */ def set(instance: JacksonJson): JacksonJson = ref.getAndSet(instance) } -private[json] case class JacksonJson(settings: JsonParserSettings) { - private lazy val mapper = (new ObjectMapper).registerModule(new PlayJsonModule(settings)) +private[json] case class JacksonJson(jsonSettings: JsonConfig) { + private lazy val mapper = (new ObjectMapper).registerModule(new PlayJsonMapperModule(jsonSettings)) private lazy val jsonFactory = new JsonFactory(mapper) 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 1f4c745f2..b609b3bf5 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 @@ -71,20 +71,21 @@ class JsonSpec extends org.specs2.mutable.Specification { val mapper = new ObjectMapper() val preserveZeroDecimal: JacksonJson = { - val defaultSerializerSettings = JsonParserSettings.settings.bigDecimalSerializerSettings - val defaultParserSettings = JsonParserSettings.settings.bigDecimalParseSettings - val serializerSettings = BigDecimalSerializerSettings( + val defaultSerializerSettings = JsonConfig.settings.bigDecimalSerializerConfig + val defaultParserSettings = JsonConfig.settings.bigDecimalParseConfig + val serializerSettings = BigDecimalSerializerConfig( defaultSerializerSettings.minPlain, defaultSerializerSettings.maxPlain, preserveZeroDecimal = true ) - JacksonJson(JsonParserSettings(defaultParserSettings, serializerSettings)) + + JacksonJson(JsonConfig(defaultParserSettings, serializerSettings)) } def withJacksonJson[T](jacksonJson: JacksonJson)(f: () => T) = { val oldInstance = JacksonJson.set(jacksonJson) try { - JacksonJson.get = jacksonJson + JacksonJson.set(jacksonJson) f.apply() } finally { JacksonJson.set(oldInstance) From a3189c545269aa352a43ed0e3af43dddc29bff42 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 29 Nov 2022 16:02:25 -0800 Subject: [PATCH 10/19] rename constants for consistency --- .../scala/play/api/libs/json/JsonConfig.scala | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 81ad713c8..6782ef444 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 play.api.libs.json.JsonConfig.MaxPlain -import play.api.libs.json.JsonConfig.MinPlain +import play.api.libs.json.JsonConfig.defaultMaxPlain +import play.api.libs.json.JsonConfig.defaultMinPlain import play.api.libs.json.JsonConfig.defaultDigitsLimit import play.api.libs.json.JsonConfig.defaultMathContext import play.api.libs.json.JsonConfig.defaultPreserveZeroDecimal @@ -61,9 +61,9 @@ sealed trait BigDecimalSerializerConfig { object BigDecimalSerializerConfig { def apply( - minPlain: BigDecimal = MinPlain, - maxPlain: BigDecimal = MaxPlain, - preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal + minPlain: BigDecimal = defaultMinPlain, + maxPlain: BigDecimal = defaultMaxPlain, + preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal ): BigDecimalSerializerConfig = DecimalSerializerSettingsImpl(minPlain, maxPlain, preserveZeroDecimal) } @@ -95,19 +95,19 @@ object JsonConfig { val defaultPreserveZeroDecimal: Boolean = false // Maximum magnitude of BigDecimal to write out as a plain string - val MaxPlain: BigDecimal = 1E20 + val defaultMaxPlain: BigDecimal = 1E20 // Minimum magnitude of BigDecimal to write out as a plain string - val MinPlain: BigDecimal = 1E-10 + val defaultMinPlain: BigDecimal = 1E-10 private[json] def loadScaleLimit: Int = parseNum("play.json.parser.scaleLimit", defaultScaleLimit)(_.toInt) private[json] def loadDigitsLimit: Int = parseNum("play.json.parser.digitsLimit", defaultDigitsLimit)(_.toInt) private[json] def loadMathContext: MathContext = parseMathContext("play.json.parser.mathContext") - private[json] def loadMinPlain: BigDecimal = parseNum("play.json.serializer.minPlain", MinPlain)(BigDecimal.exact) + private[json] def loadMinPlain: BigDecimal = parseNum("play.json.serializer.minPlain", defaultMinPlain)(BigDecimal.exact) - private[json] def loadMaxPlain: BigDecimal = parseNum("play.json.serializer.maxPlain", MaxPlain)(BigDecimal.exact) + private[json] def loadMaxPlain: BigDecimal = parseNum("play.json.serializer.maxPlain", defaultMaxPlain)(BigDecimal.exact) private[json] def loadPreserveZeroDecimal: Boolean = parseNum("play.json.serializer.preserveZeroDecimal", defaultPreserveZeroDecimal)(_.toBoolean) @@ -185,10 +185,10 @@ object JsonParserSettings { val defaultDigitsLimit: Int = JsonConfig.defaultDigitsLimit // Maximum magnitude of BigDecimal to write out as a plain string - val MaxPlain: BigDecimal = JsonConfig.MaxPlain + val MaxPlain: BigDecimal = JsonConfig.defaultMaxPlain // Minimum magnitude of BigDecimal to write out as a plain string - val MinPlain: BigDecimal = JsonConfig.MinPlain + val MinPlain: BigDecimal = JsonConfig.defaultMinPlain def apply(): JsonParserSettings = JsonParserSettings( BigDecimalParseSettings(defaultMathContext, defaultScaleLimit, defaultDigitsLimit), From 004edea7230d71997e38960d43f0e89c9ec0ea09 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Tue, 29 Nov 2022 16:06:37 -0800 Subject: [PATCH 11/19] fix formatting --- .../main/scala/play/api/libs/json/JsonConfig.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 6782ef444..8e4946005 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 @@ -61,9 +61,9 @@ sealed trait BigDecimalSerializerConfig { object BigDecimalSerializerConfig { def apply( - minPlain: BigDecimal = defaultMinPlain, - maxPlain: BigDecimal = defaultMaxPlain, - preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal + minPlain: BigDecimal = defaultMinPlain, + maxPlain: BigDecimal = defaultMaxPlain, + preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal ): BigDecimalSerializerConfig = DecimalSerializerSettingsImpl(minPlain, maxPlain, preserveZeroDecimal) } @@ -105,9 +105,11 @@ object JsonConfig { private[json] def loadMathContext: MathContext = parseMathContext("play.json.parser.mathContext") - private[json] def loadMinPlain: BigDecimal = parseNum("play.json.serializer.minPlain", defaultMinPlain)(BigDecimal.exact) + private[json] def loadMinPlain: BigDecimal = + parseNum("play.json.serializer.minPlain", defaultMinPlain)(BigDecimal.exact) - private[json] def loadMaxPlain: BigDecimal = parseNum("play.json.serializer.maxPlain", defaultMaxPlain)(BigDecimal.exact) + private[json] def loadMaxPlain: BigDecimal = + parseNum("play.json.serializer.maxPlain", defaultMaxPlain)(BigDecimal.exact) private[json] def loadPreserveZeroDecimal: Boolean = parseNum("play.json.serializer.preserveZeroDecimal", defaultPreserveZeroDecimal)(_.toBoolean) From f731f2015f8eeda6e03ea652b5d96767ecda28fe Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Mon, 5 Dec 2022 12:03:08 -0800 Subject: [PATCH 12/19] fix bug stripping zeros for powers of ten, add tests --- .../scala/play/api/libs/json/JsonConfig.scala | 1 + .../api/libs/json/jackson/JacksonJson.scala | 7 ++-- .../scala/play/api/libs/json/JsonSpec.scala | 32 ++++++++++++++++++- 3 files changed, 34 insertions(+), 6 deletions(-) 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 8e4946005..42a311693 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 @@ -101,6 +101,7 @@ object JsonConfig { val defaultMinPlain: BigDecimal = 1E-10 private[json] def loadScaleLimit: Int = parseNum("play.json.parser.scaleLimit", defaultScaleLimit)(_.toInt) + private[json] def loadDigitsLimit: Int = parseNum("play.json.parser.digitsLimit", defaultDigitsLimit)(_.toInt) private[json] def loadMathContext: MathContext = parseMathContext("play.json.parser.mathContext") 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 4a5e406dd..810738e57 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 @@ -6,13 +6,11 @@ package play.api.libs.json.jackson import java.io.InputStream import java.io.StringWriter - import scala.annotation.switch import scala.annotation.tailrec import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ListBuffer - import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser @@ -20,16 +18,15 @@ 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.module.SimpleModule import com.fasterxml.jackson.databind.ser.Serializers - import play.api.libs.json._ +import java.math.BigDecimal.ONE import java.util.concurrent.atomic.AtomicReference /** @@ -77,7 +74,7 @@ private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSer private def stripTrailingZeros(bigDec: JBigDec): JBigDec = { val stripped = bigDec.stripTrailingZeros - if (jsonConfig.bigDecimalSerializerConfig.preserveZeroDecimal && bigDec.scale > 0 && stripped.scale == 0) { + if (jsonConfig.bigDecimalSerializerConfig.preserveZeroDecimal && bigDec.scale > 0 && stripped.scale <= 0) { // restore .0 if rounded to a whole number stripped.setScale(1) } else { 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 b609b3bf5..70c27d79a 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 @@ -321,6 +321,21 @@ class JsonSpec extends org.specs2.mutable.Specification { s.mustEqual("0") } + "drop multiple trailing zero decimals from multiple of ten" in { + val s = stringify(toJson(BigDecimal("10.00"))) + s.mustEqual("10") + } + + "integer multiple of ten unchanged" in { + val s = stringify(toJson(BigDecimal("10"))) + s.mustEqual("10") + } + + "integer zero unchanged" in { + val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0")))) + s.mustEqual("0") + } + "drop multiple trailing zeros for non-zero decimal with preserveZeroDecimal=true" in { val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.020300")))) s.mustEqual("1.0203") @@ -336,11 +351,26 @@ class JsonSpec extends org.specs2.mutable.Specification { s.mustEqual("1.0") } - "preserve a single trailing zero decimal from zero value with preserveZeroDecimal=true" in { + "preserve a single trailing zero decimal from zero decimal with preserveZeroDecimal=true" in { val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0.00")))) s.mustEqual("0.0") } + "preserve a single trailing zero decimal from multiple of ten with preserveZeroDecimal=true" in { + val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("10.00")))) + s.mustEqual("10.0") + } + + "integer multiple of ten with preserveZeroDecimal=true" in { + val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("10")))) + s.mustEqual("10") + } + + "integer zero with preserveZeroDecimal=true" in { + val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0")))) + s.mustEqual("0") + } + "success when not exceeding the scale limit for positive numbers" in { val withinScaleLimit = BigDecimal(2, parserSettings.bigDecimalParseSettings.scaleLimit - 1) Json From 6d4ac29167a8eca42441e0d56a3eabd2023b9116 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Mon, 5 Dec 2022 14:58:35 -0800 Subject: [PATCH 13/19] formatting --- .../jvm/src/main/scala/play/api/libs/json/JsonConfig.scala | 2 +- .../src/main/scala/play/api/libs/json/jackson/JacksonJson.scala | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 42a311693..ee4800b37 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 @@ -100,7 +100,7 @@ object JsonConfig { // Minimum magnitude of BigDecimal to write out as a plain string val defaultMinPlain: BigDecimal = 1E-10 - private[json] def loadScaleLimit: Int = parseNum("play.json.parser.scaleLimit", defaultScaleLimit)(_.toInt) + private[json] def loadScaleLimit: Int = parseNum("play.json.parser.scaleLimit", defaultScaleLimit)(_.toInt) private[json] def loadDigitsLimit: Int = parseNum("play.json.parser.digitsLimit", defaultDigitsLimit)(_.toInt) 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 810738e57..01c1e76ff 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 @@ -26,7 +26,6 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.Serializers import play.api.libs.json._ -import java.math.BigDecimal.ONE import java.util.concurrent.atomic.AtomicReference /** From 894ba307a50f1a4795835dbb921db93881f22d2d Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Mon, 5 Dec 2022 15:00:50 -0800 Subject: [PATCH 14/19] explicitly throw exception from try-finally --- play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala | 2 ++ 1 file changed, 2 insertions(+) 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 70c27d79a..71387ce2f 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 @@ -87,6 +87,8 @@ class JsonSpec extends org.specs2.mutable.Specification { try { JacksonJson.set(jacksonJson) f.apply() + } catch { + case err: Throwable => throw err } finally { JacksonJson.set(oldInstance) } From 1c4f3255bf7c7723a0133a2a95e5bf1753e35cbd Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Wed, 4 Jan 2023 20:33:49 -0800 Subject: [PATCH 15/19] remove atomic reference, document configuration --- .../scala/play/api/libs/json/JsonConfig.scala | 119 ++++++++++++++---- .../api/libs/json/jackson/JacksonJson.scala | 24 ++-- .../scala/play/api/libs/json/JsonSpec.scala | 27 ++-- 3 files changed, 119 insertions(+), 51 deletions(-) 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 ee4800b37..6c2147e20 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 @@ -21,18 +21,30 @@ import java.math.MathContext import scala.util.control.NonFatal /** - * Parse settings for BigDecimals. Defines limits that will be used when parsing the BigDecimals, like how many digits - * are accepted. + * Parse and serialization settings for BigDecimals. Defines limits that will be used when parsing the BigDecimals, + * like how many digits are accepted. */ sealed trait BigDecimalParseConfig { - /** The [[MathContext]] used when parsing. */ + /** + * The [[MathContext]] used when parsing, which will be "decimal32", "decimal64", "decimal128" (default), + * or "unlimited". + * This can be set using the [[JsonConfig.mathContextProperty]] system property. + */ def mathContext: MathContext - /** Limits the scale, and it is related to the math context used. */ + /** + * Limits the scale, and it is related to the math context used. + * The default value is [[JsonConfig.defaultScaleLimit]]. + * This can be set using the [[JsonConfig.scaleLimitProperty]] system property. + */ def scaleLimit: Int - /** How many digits are accepted, also related to the math context used. */ + /** + * How many digits are accepted, also related to the math context used. + * The default value is [[JsonConfig.defaultDigitsLimit]]. + * This can be set using the [[JsonConfig.digitsLimitProperty]] system property. + */ def digitsLimit: Int } @@ -49,13 +61,27 @@ private final case class BigDecimalParseConfigImpl(mathContext: MathContext, sca sealed trait BigDecimalSerializerConfig { - /** Minimum magnitude of BigDecimal to write out as a plain string. */ + /** + * Minimum magnitude of BigDecimal to write out as a plain string. + * Defaults to [[JsonConfig.defaultMinPlain]]. + * This can be set using the [[JsonConfig.minPlainProperty]] system property. + */ def minPlain: BigDecimal - /** Maximum magnitude of BigDecimal to write out as a plain string. */ + /** + * Maximum magnitude of BigDecimal to write out as a plain string. + * Defaults to [[JsonConfig.defaultMaxPlain]]. + * This can be set using the [[JsonConfig.maxPlainProperty]] system property. + */ def maxPlain: BigDecimal - /** True to preserve zero decimals (false by default). */ + /** + * True to preserve a zero decimal , or false to drop them (the default). + * For example, 1.00 will be serialized as 1 if false or 1.0 if true (only a single zero is preserved). + * Other trailing zeroes will be dropped regardless of this value. + * For example, 1.1000 will always be serialized as 1.1. + * This can be set using the [[JsonConfig.preserveZeroDecimalProperty]] system property. + */ def preserveZeroDecimal: Boolean } @@ -80,41 +106,90 @@ sealed trait JsonConfig { } object JsonConfig { + + /** + * The default math context ("decimal128"). + */ val defaultMathContext: MathContext = MathContext.DECIMAL128 - // Limit for the scale considering the MathContext of 128 - // limit for scale for decimal128: BigDecimal("0." + "0" * 33 + "1e-6143", java.math.MathContext.DECIMAL128).scale + 1 + /** + * The default limit for the scale considering the default MathContext of decimal128. + * limit for scale for decimal128: BigDecimal("0." + "0" * 33 + "1e-6143", java.math.MathContext.DECIMAL128).scale + 1 + */ val defaultScaleLimit: Int = 6178 - // 307 digits should be the correct value for 128 bytes. But we are using 310 - // because Play JSON uses BigDecimal to parse any number including Doubles and - // Doubles max value has 309 digits, so we are using 310 here + /** + * The default limit for digits considering the default MathContext of decimal128. + * 307 digits should be the correct value for 128 bytes. But we are using 310 + * because Play JSON uses BigDecimal to parse any number including Doubles and + * Doubles max value has 309 digits, so we are using 310 here + */ val defaultDigitsLimit: Int = 310 - // Drop zero decimal by default. + /** + * Zero decimal values (e.g. .0 or .00) or dropped by default. + * For example, a value of 1.0 or 1.00 will be serialized as 1. + */ val defaultPreserveZeroDecimal: Boolean = false - // Maximum magnitude of BigDecimal to write out as a plain string + /** + * The default maximum magnitude of BigDecimal to write out as a plain string. + */ val defaultMaxPlain: BigDecimal = 1E20 - // Minimum magnitude of BigDecimal to write out as a plain string + /** + * The default minimum magnitude of BigDecimal to write out as a plain string. + */ val defaultMinPlain: BigDecimal = 1E-10 - private[json] def loadScaleLimit: Int = parseNum("play.json.parser.scaleLimit", defaultScaleLimit)(_.toInt) + /** + * The system property to override the scale limit. + */ + val scaleLimitProperty: String = "play.json.parser.scaleLimit" + + /** + * The system property to override the digits limit + */ + val digitsLimitProperty: String = "play.json.parser.digitsLimit" + + /** + * The system property to override the math context. This can be "decimal32", "decimal64", "decimal128" (the default), + * or "unlimited". + */ + val mathContextProperty: String = "play.json.parser.mathContext" + + /** + * The system property to override the minimum magnitude of BigDecimal to write out as a plain string + */ + val minPlainProperty: String = "play.json.serializer.minPlain" + + /** + * The system property to override the maximum magnitude of BigDecimal to write out as a plain string + */ + val maxPlainProperty: String = "play.json.serializer.maxPlain" + + /** + * The system property to override whether zero decimals (e.g. .0 or .00) are written by default. These are dropped by default. + */ + val preserveZeroDecimalProperty: String = "play.json.serializer.preserveZeroDecimal" + + private[json] def loadScaleLimit: Int = parseNum(scaleLimitProperty, defaultScaleLimit)(_.toInt) - private[json] def loadDigitsLimit: Int = parseNum("play.json.parser.digitsLimit", defaultDigitsLimit)(_.toInt) + private[json] def loadDigitsLimit: Int = parseNum(digitsLimitProperty, defaultDigitsLimit)(_.toInt) - private[json] def loadMathContext: MathContext = parseMathContext("play.json.parser.mathContext") + private[json] def loadMathContext: MathContext = parseMathContext(mathContextProperty) private[json] def loadMinPlain: BigDecimal = - parseNum("play.json.serializer.minPlain", defaultMinPlain)(BigDecimal.exact) + parseNum(minPlainProperty, defaultMinPlain)(BigDecimal.exact) private[json] def loadMaxPlain: BigDecimal = - parseNum("play.json.serializer.maxPlain", defaultMaxPlain)(BigDecimal.exact) + parseNum(maxPlainProperty, defaultMaxPlain)(BigDecimal.exact) private[json] def loadPreserveZeroDecimal: Boolean = - parseNum("play.json.serializer.preserveZeroDecimal", defaultPreserveZeroDecimal)(_.toBoolean) + parseNum(preserveZeroDecimalProperty, defaultPreserveZeroDecimal)(_.toBoolean) + // Default settings, which can be controlled with system properties. + // To override, call JacksonJson.setConfig() val settings: JsonConfig = JsonConfig( BigDecimalParseConfig(loadMathContext, loadScaleLimit, loadDigitsLimit), 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 01c1e76ff..6452ff721 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 @@ -26,8 +26,6 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.Serializers import play.api.libs.json._ -import java.util.concurrent.atomic.AtomicReference - /** * The Play JSON module for Jackson. * @@ -269,24 +267,20 @@ private[jackson] class PlaySerializers(jsonSettings: JsonConfig) extends Seriali } private[json] object JacksonJson { - val defaultInstance: JacksonJson = JacksonJson(JsonConfig.settings) - - private val ref: AtomicReference[JacksonJson] = new AtomicReference[JacksonJson](defaultInstance) + private var instance = JacksonJson(JsonConfig.settings) - /** - * Instance used to serialize and deserialize JSON. This is configured with system properties, but can be - * overridden for testing. - */ - def get: JacksonJson = ref.get + /** Overrides the config. */ + private[json] def setConfig(jsonConfig: JsonConfig): Unit = { + instance = JacksonJson(jsonConfig) + } - /** Sets the instance for testing and returns the old value. */ - def set(instance: JacksonJson): JacksonJson = ref.getAndSet(instance) + private[json] def get: JacksonJson = instance } -private[json] case class JacksonJson(jsonSettings: JsonConfig) { - private lazy val mapper = (new ObjectMapper).registerModule(new PlayJsonMapperModule(jsonSettings)) +private[json] case class JacksonJson(jsonConfig: JsonConfig) { + private val mapper = (new ObjectMapper).registerModule(new PlayJsonMapperModule(jsonConfig)) - private lazy val jsonFactory = new JsonFactory(mapper) + private val jsonFactory = new JsonFactory(mapper) private def stringJsonGenerator(out: java.io.StringWriter) = jsonFactory.createGenerator(out) 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 71387ce2f..d5a2fb42a 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 @@ -70,7 +70,7 @@ class JsonSpec extends org.specs2.mutable.Specification { val mapper = new ObjectMapper() - val preserveZeroDecimal: JacksonJson = { + val preserveZeroDecimal: JsonConfig = { val defaultSerializerSettings = JsonConfig.settings.bigDecimalSerializerConfig val defaultParserSettings = JsonConfig.settings.bigDecimalParseConfig val serializerSettings = BigDecimalSerializerConfig( @@ -79,18 +79,17 @@ class JsonSpec extends org.specs2.mutable.Specification { preserveZeroDecimal = true ) - JacksonJson(JsonConfig(defaultParserSettings, serializerSettings)) + JsonConfig(defaultParserSettings, serializerSettings) } - def withJacksonJson[T](jacksonJson: JacksonJson)(f: () => T) = { - val oldInstance = JacksonJson.set(jacksonJson) + def withJsonConfig[T](jsonConfig: JsonConfig)(f: () => T) = { try { - JacksonJson.set(jacksonJson) + JacksonJson.setConfig(jsonConfig) f.apply() } catch { case err: Throwable => throw err } finally { - JacksonJson.set(oldInstance) + JacksonJson.setConfig(JsonConfig.settings) } } @@ -334,42 +333,42 @@ class JsonSpec extends org.specs2.mutable.Specification { } "integer zero unchanged" in { - val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0")))) + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0")))) s.mustEqual("0") } "drop multiple trailing zeros for non-zero decimal with preserveZeroDecimal=true" in { - val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.020300")))) + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.020300")))) s.mustEqual("1.0203") } "do not drop single trailing zero decimal with preserveZeroDecimal=true" in { - val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.0")))) + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.0")))) s.mustEqual("1.0") } "preserve a single trailing zero decimal with preserveZeroDecimal=true" in { - val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.00")))) + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.00")))) s.mustEqual("1.0") } "preserve a single trailing zero decimal from zero decimal with preserveZeroDecimal=true" in { - val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0.00")))) + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0.00")))) s.mustEqual("0.0") } "preserve a single trailing zero decimal from multiple of ten with preserveZeroDecimal=true" in { - val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("10.00")))) + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("10.00")))) s.mustEqual("10.0") } "integer multiple of ten with preserveZeroDecimal=true" in { - val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("10")))) + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("10")))) s.mustEqual("10") } "integer zero with preserveZeroDecimal=true" in { - val s = withJacksonJson(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0")))) + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0")))) s.mustEqual("0") } From f024b1b1cb2de25722d3799d752499a8db5283f2 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Wed, 4 Jan 2023 20:43:12 -0800 Subject: [PATCH 16/19] Update play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cédric Chantepie --- .../jvm/src/main/scala/play/api/libs/json/JsonConfig.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6c2147e20..8bd0f8cc5 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 @@ -212,7 +212,7 @@ object JsonConfig { case _ => defaultMathContext } - private[json] def parseNum[T](key: String, default: T)(f: String => T): T = + private[json] def numericProp[T](key: String, default: T)(f: String => T): T = try { sys.props.get(key).map(f).getOrElse(default) } catch { From b64a6058df6591d27d423a94f3428cb84d1bc9f7 Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Wed, 4 Jan 2023 20:56:48 -0800 Subject: [PATCH 17/19] fix build, rename method --- .../main/scala/play/api/libs/json/JsonConfig.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 8bd0f8cc5..137aa9328 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 @@ -173,20 +173,18 @@ object JsonConfig { */ val preserveZeroDecimalProperty: String = "play.json.serializer.preserveZeroDecimal" - private[json] def loadScaleLimit: Int = parseNum(scaleLimitProperty, defaultScaleLimit)(_.toInt) + private[json] def loadScaleLimit: Int = prop(scaleLimitProperty, defaultScaleLimit)(_.toInt) - private[json] def loadDigitsLimit: Int = parseNum(digitsLimitProperty, defaultDigitsLimit)(_.toInt) + private[json] def loadDigitsLimit: Int = prop(digitsLimitProperty, defaultDigitsLimit)(_.toInt) private[json] def loadMathContext: MathContext = parseMathContext(mathContextProperty) - private[json] def loadMinPlain: BigDecimal = - parseNum(minPlainProperty, defaultMinPlain)(BigDecimal.exact) + private[json] def loadMinPlain: BigDecimal = prop(minPlainProperty, defaultMinPlain)(BigDecimal.exact) - private[json] def loadMaxPlain: BigDecimal = - parseNum(maxPlainProperty, defaultMaxPlain)(BigDecimal.exact) + private[json] def loadMaxPlain: BigDecimal = prop(maxPlainProperty, defaultMaxPlain)(BigDecimal.exact) private[json] def loadPreserveZeroDecimal: Boolean = - parseNum(preserveZeroDecimalProperty, defaultPreserveZeroDecimal)(_.toBoolean) + prop(preserveZeroDecimalProperty, defaultPreserveZeroDecimal)(_.toBoolean) // Default settings, which can be controlled with system properties. // To override, call JacksonJson.setConfig() @@ -212,7 +210,7 @@ object JsonConfig { case _ => defaultMathContext } - private[json] def numericProp[T](key: String, default: T)(f: String => T): T = + private[json] def prop[T](key: String, default: T)(f: String => T): T = try { sys.props.get(key).map(f).getOrElse(default) } catch { From 5ad9f534d9fe223a676b90e240065b6117d8950d Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Mon, 9 Jan 2023 09:06:13 -0800 Subject: [PATCH 18/19] formatting --- .../main/scala/play/api/libs/json/jackson/JacksonJson.scala | 4 ++++ 1 file changed, 4 insertions(+) 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 6452ff721..58eba7865 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 @@ -6,11 +6,13 @@ package play.api.libs.json.jackson import java.io.InputStream import java.io.StringWriter + import scala.annotation.switch import scala.annotation.tailrec import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ListBuffer + import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser @@ -18,12 +20,14 @@ 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.module.SimpleModule import com.fasterxml.jackson.databind.ser.Serializers + import play.api.libs.json._ /** From ae7912fa1a87f86a027d9068d1b93811e027d1bb Mon Sep 17 00:00:00 2001 From: Richard Bogart Date: Thu, 12 Jan 2023 15:54:41 -0800 Subject: [PATCH 19/19] update deprecated since values --- .../jvm/src/main/scala/play/api/libs/json/JsonConfig.scala | 6 +++--- .../main/scala/play/api/libs/json/jackson/JacksonJson.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 137aa9328..1db2a58f4 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 @@ -223,14 +223,14 @@ private final case class JsonConfigImpl( bigDecimalSerializerConfig: BigDecimalSerializerConfig ) extends JsonConfig -@deprecated("Use BigDecimalParseConfig instead", "2.10.0") +@deprecated("Use BigDecimalParseConfig instead", "2.9.4") final case class BigDecimalParseSettings( mathContext: MathContext = MathContext.DECIMAL128, scaleLimit: Int, digitsLimit: Int ) extends BigDecimalParseConfig -@deprecated("Use BigDecimalSerializerConfig instead", "2.10.0") +@deprecated("Use BigDecimalSerializerConfig instead", "2.9.4") final case class BigDecimalSerializerSettings( minPlain: BigDecimal, maxPlain: BigDecimal @@ -238,7 +238,7 @@ final case class BigDecimalSerializerSettings( override def preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal } -@deprecated("Use JsonConfig instead", "2.10.0") +@deprecated("Use JsonConfig instead", "2.9.4") final case class JsonParserSettings( bigDecimalParseSettings: BigDecimalParseSettings, bigDecimalSerializerSettings: BigDecimalSerializerSettings 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 58eba7865..6ffa66c42 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 @@ -49,7 +49,7 @@ import play.api.libs.json._ * val jsValue = mapper.readValue("""{"foo":"bar"}""", classOf[JsValue]) * }}} */ -@deprecated("Use PlayJsonMapperModule class instead", "2.10.0") +@deprecated("Use PlayJsonMapperModule class instead", "2.9.4") sealed class PlayJsonModule(parserSettings: JsonParserSettings) extends PlayJsonMapperModule(parserSettings) { override def setupModule(context: SetupContext): Unit = super.setupModule(context) }