Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to preserve zero decimals #831

Merged
merged 19 commits into from
Jan 17, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ trait EnvReads {
*/
implicit object JsonNodeReads extends Reads[JsonNode] {
def reads(json: JsValue): JsResult[JsonNode] =
JsSuccess(JacksonJson.jsValueToJsonNode(json))
JsSuccess(JacksonJson.get.jsValueToJsonNode(json))
}

/**
* Deserializer for Jackson ObjectNode
*/
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.get.jsValueToJsonNode(jo).asInstanceOf[ObjectNode])
}
}

Expand All @@ -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.get.jsValueToJsonNode(ja).asInstanceOf[ArrayNode])
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.get.jsonNodeToJsValue(o)
}

/**
* Serializer for Jackson JsonNode
*/
implicit def jsonNodeWrites[T <: JsonNode]: Writes[T] =
Writes[T](JacksonJson.jsonNodeToJsValue)
Writes[T](JacksonJson.get.jsonNodeToJsValue)

/** Typeclass to implement way of formatting of Java8 temporal types. */
trait TemporalFormatter[T <: Temporal] {
Expand Down
210 changes: 210 additions & 0 deletions play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Copyright (C) 2009-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed from JsonParserSettings.

I ended up using sealed traits, which I called JsonConfig, BigDecimalParseConfig, and BigDecimalSerializerConfig.


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 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 = defaultMinPlain,
maxPlain: BigDecimal = defaultMaxPlain,
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 defaultMaxPlain: BigDecimal = 1E20

// 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 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", defaultMinPlain)(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)

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 =
trbogart marked this conversation as resolved.
Show resolved Hide resolved
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")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should I use for the 2nd argument of these deprecated annotations?

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.defaultMaxPlain

// Minimum magnitude of BigDecimal to write out as a plain string
val MinPlain: BigDecimal = JsonConfig.defaultMinPlain

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)
)
}
}

This file was deleted.

Loading