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/EnvReads.scala b/play-json/jvm/src/main/scala/play/api/libs/json/EnvReads.scala index 7e5c592b0..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.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.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.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 92d21f1eb..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.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] { 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..1db2a58f4 --- /dev/null +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +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 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, 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. + * 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. + * The default value is [[JsonConfig.defaultDigitsLimit]]. + * This can be set using the [[JsonConfig.digitsLimitProperty]] system property. + */ + 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. + * 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. + * Defaults to [[JsonConfig.defaultMaxPlain]]. + * This can be set using the [[JsonConfig.maxPlainProperty]] system property. + */ + def maxPlain: BigDecimal + + /** + * 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 +} + +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 { + + /** + * The default math context ("decimal128"). + */ + val defaultMathContext: MathContext = MathContext.DECIMAL128 + + /** + * 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 + + /** + * 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 + + /** + * 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 + + /** + * The default maximum magnitude of BigDecimal to write out as a plain string. + */ + val defaultMaxPlain: BigDecimal = 1E20 + + /** + * The default minimum magnitude of BigDecimal to write out as a plain string. + */ + val defaultMinPlain: BigDecimal = 1E-10 + + /** + * 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 = prop(scaleLimitProperty, defaultScaleLimit)(_.toInt) + + private[json] def loadDigitsLimit: Int = prop(digitsLimitProperty, defaultDigitsLimit)(_.toInt) + + private[json] def loadMathContext: MathContext = parseMathContext(mathContextProperty) + + private[json] def loadMinPlain: BigDecimal = prop(minPlainProperty, defaultMinPlain)(BigDecimal.exact) + + private[json] def loadMaxPlain: BigDecimal = prop(maxPlainProperty, defaultMaxPlain)(BigDecimal.exact) + + private[json] def loadPreserveZeroDecimal: Boolean = + prop(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), + 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 prop[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.9.4") +final case class BigDecimalParseSettings( + mathContext: MathContext = MathContext.DECIMAL128, + scaleLimit: Int, + digitsLimit: Int +) extends BigDecimalParseConfig + +@deprecated("Use BigDecimalSerializerConfig instead", "2.9.4") +final case class BigDecimalSerializerSettings( + minPlain: BigDecimal, + maxPlain: BigDecimal +) extends BigDecimalSerializerConfig { + override def preserveZeroDecimal: Boolean = defaultPreserveZeroDecimal +} + +@deprecated("Use JsonConfig instead", "2.9.4") +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) + ) + } +} 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 28b5b6aa3..000000000 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2009-2021 Lightbend Inc. - */ - -package play.api.libs.json - -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 -) - -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 - - // 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) - ) - - /** - * Return the parse settings that are configured. - */ - 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) - - JsonParserSettings( - BigDecimalParseSettings( - mathContext, - scaleLimit, - digitsLimit - ), - BigDecimalSerializerSettings( - minPlain, - maxPlain - ) - ) - } - - 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/StaticBinding.scala b/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala index 4b77f6268..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.parseJsValue(data) + JacksonJson.get.parseJsValue(data) /** Parses a [[JsValue]] from a string content. */ def parseJsValue(input: String): JsValue = - JacksonJson.parseJsValue(input) + JacksonJson.get.parseJsValue(input) /** Parses a [[JsValue]] from a stream. */ def parseJsValue(stream: java.io.InputStream): JsValue = - JacksonJson.parseJsValue(stream) + JacksonJson.get.parseJsValue(stream) def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String = - JacksonJson.generateFromJsValue(jsValue, escapeNonASCII) + JacksonJson.get.generateFromJsValue(jsValue, escapeNonASCII) - def prettyPrint(jsValue: JsValue): String = JacksonJson.prettyPrint(jsValue) + def prettyPrint(jsValue: JsValue): String = JacksonJson.get.prettyPrint(jsValue) def toBytes(jsValue: JsValue): Array[Byte] = - JacksonJson.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 91384fb34..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 @@ -43,32 +43,46 @@ import play.api.libs.json._ * 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.9.4") +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 } 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 (jsonConfig.bigDecimalSerializerConfig.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) => { @@ -77,9 +91,9 @@ 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 = 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) @@ -136,7 +150,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 @@ -153,7 +167,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) @@ -236,19 +250,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 } @@ -257,9 +271,20 @@ private[jackson] class PlaySerializers(parserSettings: JsonParserSettings) exten } private[json] object JacksonJson { - private lazy val mapper = (new ObjectMapper).registerModule(new PlayJsonModule(JsonParserSettings.settings)) + private var instance = JacksonJson(JsonConfig.settings) + + /** Overrides the config. */ + private[json] def setConfig(jsonConfig: JsonConfig): Unit = { + instance = JacksonJson(jsonConfig) + } + + private[json] def get: JacksonJson = instance +} + +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 bc3813c0c..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 @@ -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,29 @@ class JsonSpec extends org.specs2.mutable.Specification { val mapper = new ObjectMapper() + val preserveZeroDecimal: JsonConfig = { + val defaultSerializerSettings = JsonConfig.settings.bigDecimalSerializerConfig + val defaultParserSettings = JsonConfig.settings.bigDecimalParseConfig + val serializerSettings = BigDecimalSerializerConfig( + defaultSerializerSettings.minPlain, + defaultSerializerSettings.maxPlain, + preserveZeroDecimal = true + ) + + JsonConfig(defaultParserSettings, serializerSettings) + } + + def withJsonConfig[T](jsonConfig: JsonConfig)(f: () => T) = { + try { + JacksonJson.setConfig(jsonConfig) + f.apply() + } catch { + case err: Throwable => throw err + } finally { + JacksonJson.setConfig(JsonConfig.settings) + } + } + "Complete JSON should create full object" >> { lazy val postDate: Date = dateParser.parse("2011-04-22T13:33:48Z") @@ -278,6 +302,76 @@ 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 zero decimals from zero value by default" in { + val s = stringify(toJson(BigDecimal("0.00"))) + 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 = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("0")))) + s.mustEqual("0") + } + + "drop multiple trailing zeros for non-zero decimal with preserveZeroDecimal=true" in { + 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 = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("1.0")))) + s.mustEqual("1.0") + } + + "preserve a single trailing zero decimal with preserveZeroDecimal=true" in { + 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 = 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 = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("10.00")))) + s.mustEqual("10.0") + } + + "integer multiple of ten with preserveZeroDecimal=true" in { + val s = withJsonConfig(preserveZeroDecimal)(() => stringify(toJson(BigDecimal("10")))) + s.mustEqual("10") + } + + "integer zero with preserveZeroDecimal=true" in { + val s = withJsonConfig(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