-
-
Notifications
You must be signed in to change notification settings - Fork 134
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
Conversation
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Outdated
Show resolved
Hide resolved
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Show resolved
Hide resolved
@@ -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 = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the core change.
38b3e53
to
b4d0b00
Compare
b4d0b00
to
892ad8c
Compare
@cchantep Thoughts? The general approach seems reasonable to me. |
play-json/jvm/src/main/scala/play/api/libs/json/JsonParserSettings.scala
Outdated
Show resolved
Hide resolved
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Show resolved
Hide resolved
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Outdated
Show resolved
Hide resolved
I'm not sure why mima is still failing. I'll have to come back to that next week. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MiMa is failing because you've changed the case class constructor to have 3 arguments rather than 2, which also affects the apply and copy methods generated by the case class.
Arguably it was a bad idea to make JsonParserSettings
a case class in #191, as it makes it inconvenient to add new settings to the case class without breaking compatibility. We can still technically work around this by manually adding copy
, apply
and this
methods with the old arguments, but it ends up being pretty ugly, and we will have to do this again every time we add another field to the case class.
So a few options are:
- Add in the extra methods for binary compatibility—these could be made
private[json]
so they are only visible publicly in the binary representation. - Option 1, plus we make both the constructor and copy method private, so in the future when adding config we only need to worry about the
apply
method. - Switch to using a (preferably sealed) trait for configuration, deprecate the old case class config, and create a new way to construct instance of the trait for config.
- Wait to introduce this breaking change in the next "major" version of play-json—which I think would be 2.10.0, and is coming up pretty soon.
Option 2 seems like the best balance between amount of work now and extensibility in the future.
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Outdated
Show resolved
Hide resolved
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Show resolved
Hide resolved
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Outdated
Show resolved
Hide resolved
* Copyright (C) 2009-2021 Lightbend Inc. <https://www.lightbend.com> | ||
*/ | ||
|
||
package play.api.libs.json |
There was a problem hiding this comment.
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
.
bigDecimalSerializerConfig: BigDecimalSerializerConfig | ||
) extends JsonConfig | ||
|
||
@deprecated("Use BigDecimalParseConfig instead", "2.10.0") |
There was a problem hiding this comment.
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?
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Show resolved
Hide resolved
* val jsValue = mapper.readValue("""{"foo":"bar"}""", classOf[JsValue]) | ||
* }}} | ||
*/ | ||
sealed class PlayJsonModule(parserSettings: JsonParserSettings) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only public usage of JsonParserSettings
. I deprecated this and replaced it with PlayJsonMapperModule
(I'm open to naming suggestions).
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Show resolved
Hide resolved
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stripTrailingZeros
can drop the scale below 0 (e.g. "10.0" will end up with a scale of -1, which needs to be corrected to 1). I also added some additional unit tests to cover this case.
play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala
Show resolved
Hide resolved
play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala
Outdated
Show resolved
Hide resolved
private lazy val mapper = (new ObjectMapper).registerModule(new PlayJsonModule(JsonParserSettings.settings)) | ||
val defaultInstance: JacksonJson = JacksonJson(JsonConfig.settings) | ||
|
||
private val ref: AtomicReference[JacksonJson] = new AtomicReference[JacksonJson](defaultInstance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it add overhead for all runtime whereas it's only useful for testing?
Couldn't the internal set logic and its synchronization be only in tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree if this is only for tests it probably doesn't need to be atomic. This is an internal API so we can control how it's used.
Also wondering if we could keep JacksonJson
an object and just have a setConfig
in there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll remove the atomic. I kept the case class as a convenience to initialize the mapper and factory together.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having the case class also means that any given operations will use a consistent set of mapper and factory, so there's not really a need for synchronization. Running tests concurrently could still give unexpected results (since it relies on static state), but the atomic reference didn't actually help with that anyway.
Co-authored-by: Cédric Chantepie <cchantep@users.noreply.github.com>
*/ | ||
sealed trait BigDecimalParseConfig { | ||
|
||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added more documentation for the configuration
@cchantep do you have any other concerns or are we good to merge this? |
I think we want to backport this to 2.9.x, so deprecating in |
Right now Play 2.8 ships with play-json 2.8.x, however I think it should be possible to manually upgrade to play-json 2.9.x, I think some people did that already, not sure though. |
Co-authored-by: Cédric Chantepie <cchantep@users.noreply.github.com>
Pull Request Checklist
Fixes
Fixes #107 by adding a configuration option to preserve zero decimal numbers from being rounded to a whole number (for example, 1.0 will not be converted to 1).
In particular, we want to use this to prevent ElasticSearch from creating a long dynamic mapping for a field that should be a double. It's not really a bug (as described in that issue), so the old behavior is preserved by default.
My first approach was to serialize this in our code base. That works, but isn't ideal. It required copying a fair bit of play-json code to override just a single line of code. Also, our code serializes Json in dozens of places, all of which had to be changed to use the new methods.
Purpose
This adds an option to preserve a .0 for decimal numbers that are rounded to a whole number (e.g. 1.0 to 1). This distinction is important in some situations, such as ES dynamic mapping or KairosDB schema creation.
Background Context
It might be harmless to do the new behavior by default, but the original issue did mention a preference for opting in to this behavior, which is reasonable.
References
Are there any relevant issues / PRs / mailing lists discussions?
#107