为什么play-json在读取/解析时会丢失精度?

10
在接下来的示例中(scala 2.11和play-json 2.13),请参考以下内容:
val j ="""{"t":2.2599999999999997868371792719699442386627197265625}"""
println((Json.parse(j) \ "t").as[BigDecimal].compare(BigDecimal("2.2599999999999997868371792719699442386627197265625")))

输出结果是-1,它们不应该相等吗?在打印解析值时,会打印四舍五入的值:println((Json.parse(j) \ "t").as[BigDecimal]) 返回 259999999999999786837179271969944
1个回答

9

问题在于默认情况下,play-json使用的Jackson解析器的MathContext设置为DECIMAL128。您可以通过将系统属性play.json.parser.mathContext设置为unlimited来解决这个问题。例如,在Scala REPL中,可以像这样设置:

scala> System.setProperty("play.json.parser.mathContext", "unlimited")
res0: String = null

scala> val j ="""{"t":2.2599999999999997868371792719699442386627197265625}"""
j: String = {"t":2.2599999999999997868371792719699442386627197265625}

scala> import play.api.libs.json.Json
import play.api.libs.json.Json

scala> val res = (Json.parse(j) \ "t").as[BigDecimal]
res: BigDecimal = 2.2599999999999997868371792719699442386627197265625

scala> val expected = BigDecimal("2.2599999999999997868371792719699442386627197265625")
expected: scala.math.BigDecimal = 2.2599999999999997868371792719699442386627197265625

scala> res.compare(expected)
res1: Int = 0

请注意,在任何引用Json之前,应首先进行setProperty。在正常的(非REPL)使用中,您可以通过命令行或其他方式通过-D设置属性。
或者,您可以使用Jawn的play-json解析支持,它可以像预期的那样直接工作。
scala> val j ="""{"t":2.2599999999999997868371792719699442386627197265625}"""
j: String = {"t":2.2599999999999997868371792719699442386627197265625}

scala> import org.typelevel.jawn.support.play.Parser
import org.typelevel.jawn.support.play.Parser

scala> val res = (Parser.parseFromString(j).get \ "t").as[BigDecimal]
res: BigDecimal = 2.2599999999999997868371792719699442386627197265625

或者你也可以切换到circe

scala> import io.circe.Decoder, io.circe.jawn.decode
import io.circe.Decoder
import io.circe.jawn.decode

scala> decode(j)(Decoder[BigDecimal].prepare(_.downField("t")))
res0: Either[io.circe.Error,BigDecimal] = Right(2.2599999999999997868371792719699442386627197265625)

在我看来,比起play-json来说,它更加负责任地处理了一系列与数字相关的边缘情况。例如:

scala> val big = "1e2147483648"
big: String = 1e2147483648

scala> io.circe.jawn.parse(big)
res0: Either[io.circe.ParsingFailure,io.circe.Json] = Right(1e2147483648)

scala> play.api.libs.json.Json.parse(big)
java.lang.NumberFormatException
  at java.math.BigDecimal.<init>(BigDecimal.java:491)
  at java.math.BigDecimal.<init>(BigDecimal.java:824)
  at scala.math.BigDecimal$.apply(BigDecimal.scala:287)
  at play.api.libs.json.jackson.JsValueDeserializer.parseBigDecimal(JacksonJson.scala:146)
  ...

但这超出了本问题的范畴。

说实话,我不确定为什么play-json默认使用DECIMAL128作为MathContext,但这是向play-json维护者提问的问题,也超出了这里的范畴。


说实话,有一些方法可以通过代码进行此配置(而不是通过系统属性),但我现在想不起来了。 - Travis Brown
请问您能否回忆一下这件事情吗?对我来说会很有帮助。谢谢。 - advocateofnone
@sashas 我错了——只有在直接使用 ObjectMapper 时才可能实现。例如,请参见此评论 - Travis Brown
1
@sashas 噢,评论中的示例代码似乎已经过时了,所以即使那也不是一个选项。我认为你只能使用系统属性了。 - Travis Brown

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接