Skip to content

Commit

Permalink
Add option to preserve zero decimals
Browse files Browse the repository at this point in the history
  • Loading branch information
Richard Bogart committed Nov 22, 2022
1 parent 749594f commit b4d0b00
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 21 deletions.
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.instance.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.instance.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.instance.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.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] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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(
Expand All @@ -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

Expand All @@ -52,11 +61,12 @@ 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
Expand All @@ -67,6 +77,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(
Expand All @@ -76,7 +88,8 @@ object JsonParserSettings {
),
BigDecimalSerializerSettings(
minPlain,
maxPlain
maxPlain,
preserveZeroDecimal
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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)
Expand Down Expand Up @@ -257,7 +267,15 @@ 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)

Expand Down
49 changes: 49 additions & 0 deletions play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit b4d0b00

Please sign in to comment.