From 775021d8978e1c51c4728f4dcb37ef3f75e8497d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Chantepie?= Date: Thu, 3 Sep 2026 17:34:05 +0200 Subject: [PATCH 1/2] Add useJacksonBigDecimalParser and useJacksonBigDecimalFastParser settings to JsonConfig --- .../scala/play/api/libs/json/JsonConfig.scala | 92 ++++++++++++++++--- .../play/api/libs/json/JsonConfigSpec.scala | 72 +++++++++++---- 2 files changed, 131 insertions(+), 33 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 38f999c90..1ac1a25a0 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,15 +4,16 @@ package play.api.libs.json -import com.fasterxml.jackson.core.StreamReadConstraints -import com.fasterxml.jackson.core.StreamWriteConstraints - -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 -import play.api.libs.json.JsonConfig.defaultScaleLimit +import com.fasterxml.jackson.core.{ StreamReadConstraints, StreamWriteConstraints } + +import play.api.libs.json.JsonConfig.{ + defaultMaxPlain, + defaultMinPlain, + defaultDigitsLimit, + defaultMathContext, + defaultPreserveZeroDecimal, + defaultScaleLimit +} import java.math.MathContext @@ -44,18 +45,57 @@ sealed trait BigDecimalParseConfig { * This can be set using the [[JsonConfig.digitsLimitProperty]] system property. */ def digitsLimit: Int + + /** + * Jackson has its own parser for `BigDecimal` + * and this can be further configured to use a fast parser. + * This can be set using the [[JsonConfig.useJacksonParserProperty]] system property. + */ + def useJacksonBigDecimalParser: Boolean + + /** + * Only used when `useJacksonBigDecimalParser` is true. + * This can be set using the [[JsonConfig.useJacksonBigDecimalFastParserProperty]] + * system property. + */ + def useJacksonBigDecimalFastParser: Boolean } object BigDecimalParseConfig { + def apply( mathContext: MathContext = defaultMathContext, scaleLimit: Int = defaultScaleLimit, - digitsLimit: Int = defaultDigitsLimit - ): BigDecimalParseConfig = BigDecimalParseConfigImpl(mathContext, scaleLimit, digitsLimit) + digitsLimit: Int = defaultDigitsLimit, + useJacksonBigDecimalParser: Boolean = false, + useJacksonBigDecimalFastParser: Boolean = false + ): BigDecimalParseConfig = BigDecimalParseConfigImpl( + mathContext, + scaleLimit, + digitsLimit, + useJacksonBigDecimalParser, + useJacksonBigDecimalFastParser + ) } -private final case class BigDecimalParseConfigImpl(mathContext: MathContext, scaleLimit: Int, digitsLimit: Int) - extends BigDecimalParseConfig +private final case class BigDecimalParseConfigImpl( + mathContext: MathContext, + scaleLimit: Int, + digitsLimit: Int, + useJacksonBigDecimalParser: Boolean, + useJacksonBigDecimalFastParser: Boolean +) extends BigDecimalParseConfig + +private object BigDecimalParseConfigImpl + extends scala.runtime.AbstractFunction3[MathContext, Int, Int, BigDecimalParseConfig] { + + @deprecated("Specify useJacksonBigDecimalParser and useJacksonBigDecimalFastParser parameters", "") + def apply( + mathContext: MathContext, + scaleLimit: Int, + digitsLimit: Int, + ): BigDecimalParseConfig = BigDecimalParseConfigImpl(mathContext, scaleLimit, digitsLimit, false, false) +} sealed trait BigDecimalSerializerConfig { @@ -152,6 +192,18 @@ object JsonConfig { */ val digitsLimitProperty: String = "play.json.parser.digitsLimit" + /** + * The system property to toggle the Jackson BigDecimal parser + */ + val useJacksonBigDecimalParserProperty: String = + "play.json.parser.useJacksonBigDecimalParser" + + /** + * The system property to toggle the fast-mode for the Jackson BigDecimal parser + */ + val useJacksonBigDecimalFastParserProperty: String = + "play.json.parser.useJacksonBigDecimalFastParser" + /** * The system property to override the math context. This can be "decimal32", "decimal64", "decimal128" (the default), * or "unlimited". @@ -195,6 +247,12 @@ object JsonConfig { private[json] def loadMathContext: MathContext = parseMathContext(mathContextProperty) + private[json] def loadUseJacksonBigDecimalParser: Boolean = + prop(useJacksonBigDecimalParserProperty, false)(_.toBoolean) + + private[json] def loadUseJacksonBigDecimalFastParser: Boolean = + prop(useJacksonBigDecimalFastParserProperty, false)(_.toBoolean) + private[json] def loadMinPlain: BigDecimal = prop(minPlainProperty, defaultMinPlain)(BigDecimal.exact) private[json] def loadMaxPlain: BigDecimal = prop(maxPlainProperty, defaultMaxPlain)(BigDecimal.exact) @@ -229,7 +287,13 @@ object JsonConfig { // To override, call JacksonJson.setConfig() val settings: JsonConfig = JsonConfig( - BigDecimalParseConfig(loadMathContext, loadScaleLimit, loadDigitsLimit), + BigDecimalParseConfig( + loadMathContext, + loadScaleLimit, + loadDigitsLimit, + loadUseJacksonBigDecimalParser, + loadUseJacksonBigDecimalFastParser + ), BigDecimalSerializerConfig(loadMinPlain, loadMaxPlain, loadPreserveZeroDecimal), defaultStreamReadConstraints, defaultStreamWriteConstraints 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..487ac2584 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 @@ -5,32 +5,66 @@ package play.api.libs.json import com.fasterxml.jackson.core.{ StreamReadConstraints, StreamWriteConstraints } + import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -class JsonConfigSpec extends AnyWordSpec with Matchers { +final class JsonConfigSpec extends AnyWordSpec with Matchers { "JsonConfig" should { - "fetch default nesting depth (parsing)" in { - JsonConfig.defaultStreamReadConstraints.getMaxNestingDepth.mustEqual(StreamReadConstraints.DEFAULT_MAX_DEPTH) + "fetch default setting".which { + "is defined for nesting depth (parsing)" in { + JsonConfig.defaultStreamReadConstraints.getMaxNestingDepth. + mustEqual(StreamReadConstraints.DEFAULT_MAX_DEPTH) + } + + "is defined for nesting depth (serializer)" in { + JsonConfig.defaultStreamWriteConstraints.getMaxNestingDepth. + mustEqual(StreamWriteConstraints.DEFAULT_MAX_DEPTH) + } + + "is defined for Jackson BigDecimal parser" in { + JsonConfig.loadUseJacksonBigDecimalParser mustEqual false + + JsonConfig.settings.bigDecimalParseConfig.useJacksonBigDecimalParser mustEqual false + + JsonConfig.loadUseJacksonBigDecimalFastParser mustEqual false + + JsonConfig.settings.bigDecimalParseConfig. + useJacksonBigDecimalFastParser mustEqual false + + } } - "fetch default nesting depth (serializer)" in { - JsonConfig.defaultStreamWriteConstraints.getMaxNestingDepth.mustEqual(StreamWriteConstraints.DEFAULT_MAX_DEPTH) + + "override nesting depth (parsing)" in withProperty(JsonConfig.maxNestingDepth, "200") { + JsonConfig.loadMaxNestingDepth mustEqual 200 } - "override nesting depth (parsing)" in { - System.setProperty(JsonConfig.maxNestingDepth, "200") - try { - JsonConfig.loadMaxNestingDepth.mustEqual(200) - } finally { - System.clearProperty(JsonConfig.maxNestingDepth) - } + + "override nesting depth (serializer)" in withProperty( + JsonConfig.maxSerializerNestingDepth, "300") { + + JsonConfig.loadMaxSerializerNestingDepth mustEqual 300 } - "override nesting depth (serializer)" in { - System.setProperty(JsonConfig.maxSerializerNestingDepth, "300") - try { - JsonConfig.loadMaxSerializerNestingDepth.mustEqual(300) - } finally { - System.clearProperty(JsonConfig.maxSerializerNestingDepth) - } + + "override use of Jackson BigDecimal parser" in withProperty( + "play.json.parser.useJacksonBigDecimalParser", "true") { + JsonConfig.loadUseJacksonBigDecimalParser mustEqual true + } + + "override the use of Jackson BigDecimal fast parser" in withProperty( + "play.json.parser.useJacksonBigDecimalFastParser", "true") { + JsonConfig.loadUseJacksonBigDecimalFastParser mustEqual true + } + } + + // --- + + private def withProperty[T](key: String, value: String)(f: => T): T = { + System.setProperty(key, value) + + try { + f + } finally { + System.clearProperty(key) } } } From b9976081e0cb76f6126698218860a36d36381fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Chantepie?= Date: Thu, 3 Sep 2026 17:44:36 +0200 Subject: [PATCH 2/2] Integrate Jackson BigDecimal parser --- .../play/api/libs/json/BigDecimalParser.scala | 19 ++++++++--- .../scala/play/api/libs/json/JsonConfig.scala | 18 +++++----- .../play/api/libs/json/JsonConfigSpec.scala | 34 ++++++++----------- 3 files changed, 39 insertions(+), 32 deletions(-) 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 7c04fd5b6..581326e0a 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 @@ -4,23 +4,34 @@ package play.api.libs.json +import java.math.{ BigDecimal => JBigDecimal } + private[json] object BigDecimalParser { - def parse(input: String, jsonConfig: JsonConfig): JsResult[java.math.BigDecimal] = { + def parse(input: String, jsonConfig: JsonConfig): JsResult[JBigDecimal] = { + import jsonConfig.bigDecimalParseConfig // 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 > jsonConfig.bigDecimalParseConfig.digitsLimit) { + if (input.length > 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, jsonConfig.bigDecimalParseConfig.mathContext) + val bigDecimal: JBigDecimal = { + if (bigDecimalParseConfig.useJacksonParser) { + com.fasterxml.jackson.core.io.NumberInput.parseBigDecimal(input, bigDecimalParseConfig.useJacksonFastParser) + + } else { + new JBigDecimal(input, bigDecimalParseConfig.mathContext) + } + } // We should also avoid numbers with scale that are out of a safe limit val scale = bigDecimal.scale - if (Math.abs(scale) > jsonConfig.bigDecimalParseConfig.scaleLimit) { + + if (Math.abs(scale) > 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 index 1ac1a25a0..715740486 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 @@ -51,14 +51,14 @@ sealed trait BigDecimalParseConfig { * and this can be further configured to use a fast parser. * This can be set using the [[JsonConfig.useJacksonParserProperty]] system property. */ - def useJacksonBigDecimalParser: Boolean + def useJacksonParser: Boolean /** * Only used when `useJacksonBigDecimalParser` is true. * This can be set using the [[JsonConfig.useJacksonBigDecimalFastParserProperty]] * system property. */ - def useJacksonBigDecimalFastParser: Boolean + def useJacksonFastParser: Boolean } object BigDecimalParseConfig { @@ -67,14 +67,14 @@ object BigDecimalParseConfig { mathContext: MathContext = defaultMathContext, scaleLimit: Int = defaultScaleLimit, digitsLimit: Int = defaultDigitsLimit, - useJacksonBigDecimalParser: Boolean = false, - useJacksonBigDecimalFastParser: Boolean = false + useJacksonParser: Boolean = false, + useJacksonFastParser: Boolean = false ): BigDecimalParseConfig = BigDecimalParseConfigImpl( mathContext, scaleLimit, digitsLimit, - useJacksonBigDecimalParser, - useJacksonBigDecimalFastParser + useJacksonParser, + useJacksonFastParser ) } @@ -82,14 +82,14 @@ private final case class BigDecimalParseConfigImpl( mathContext: MathContext, scaleLimit: Int, digitsLimit: Int, - useJacksonBigDecimalParser: Boolean, - useJacksonBigDecimalFastParser: Boolean + useJacksonParser: Boolean, + useJacksonFastParser: Boolean ) extends BigDecimalParseConfig private object BigDecimalParseConfigImpl extends scala.runtime.AbstractFunction3[MathContext, Int, Int, BigDecimalParseConfig] { - @deprecated("Specify useJacksonBigDecimalParser and useJacksonBigDecimalFastParser parameters", "") + @deprecated("Specify useJacksonParser and useJacksonFastParser parameters", "") def apply( mathContext: MathContext, scaleLimit: Int, 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 487ac2584..5892994dc 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 @@ -13,46 +13,42 @@ final class JsonConfigSpec extends AnyWordSpec with Matchers { "JsonConfig" should { "fetch default setting".which { "is defined for nesting depth (parsing)" in { - JsonConfig.defaultStreamReadConstraints.getMaxNestingDepth. - mustEqual(StreamReadConstraints.DEFAULT_MAX_DEPTH) + JsonConfig.defaultStreamReadConstraints.getMaxNestingDepth.mustEqual(StreamReadConstraints.DEFAULT_MAX_DEPTH) } "is defined for nesting depth (serializer)" in { - JsonConfig.defaultStreamWriteConstraints.getMaxNestingDepth. - mustEqual(StreamWriteConstraints.DEFAULT_MAX_DEPTH) + JsonConfig.defaultStreamWriteConstraints.getMaxNestingDepth.mustEqual(StreamWriteConstraints.DEFAULT_MAX_DEPTH) } "is defined for Jackson BigDecimal parser" in { - JsonConfig.loadUseJacksonBigDecimalParser mustEqual false + JsonConfig.loadUseJacksonBigDecimalParser.mustEqual(false) - JsonConfig.settings.bigDecimalParseConfig.useJacksonBigDecimalParser mustEqual false + JsonConfig.settings.bigDecimalParseConfig.useJacksonParser.mustEqual(false) - JsonConfig.loadUseJacksonBigDecimalFastParser mustEqual false - - JsonConfig.settings.bigDecimalParseConfig. - useJacksonBigDecimalFastParser mustEqual false + JsonConfig.loadUseJacksonBigDecimalFastParser.mustEqual(false) + JsonConfig.settings.bigDecimalParseConfig.useJacksonFastParser.mustEqual(false) } } "override nesting depth (parsing)" in withProperty(JsonConfig.maxNestingDepth, "200") { - JsonConfig.loadMaxNestingDepth mustEqual 200 + JsonConfig.loadMaxNestingDepth.mustEqual(200) } - "override nesting depth (serializer)" in withProperty( - JsonConfig.maxSerializerNestingDepth, "300") { + "override nesting depth (serializer)" in withProperty(JsonConfig.maxSerializerNestingDepth, "300") { - JsonConfig.loadMaxSerializerNestingDepth mustEqual 300 + JsonConfig.loadMaxSerializerNestingDepth.mustEqual(300) } - "override use of Jackson BigDecimal parser" in withProperty( - "play.json.parser.useJacksonBigDecimalParser", "true") { - JsonConfig.loadUseJacksonBigDecimalParser mustEqual true + "override use of Jackson BigDecimal parser" in withProperty("play.json.parser.useJacksonBigDecimalParser", "true") { + JsonConfig.loadUseJacksonBigDecimalParser.mustEqual(true) } "override the use of Jackson BigDecimal fast parser" in withProperty( - "play.json.parser.useJacksonBigDecimalFastParser", "true") { - JsonConfig.loadUseJacksonBigDecimalFastParser mustEqual true + "play.json.parser.useJacksonBigDecimalFastParser", + "true" + ) { + JsonConfig.loadUseJacksonBigDecimalFastParser.mustEqual(true) } }