Почему play-json теряет точность при чтении/анализе?

В следующем примере (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


person sashas    schedule 17.03.2019    source источник


Ответы (1)


Проблема в том, что по умолчанию play-json настраивает парсер Джексона с 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

Обратите внимание, что setProperty должно быть первым, перед любой ссылкой на Json. При обычном (не 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, и здесь он также выходит за рамки.

person Travis Brown    schedule 17.03.2019
comment
Что бы это ни стоило, есть какой-то способ сделать эту конфигурацию в коде (не через системные свойства), но я не помню этого сразу. - person Travis Brown; 17.03.2019
comment
Не могли бы вы посмотреть, помните ли вы это. Это было бы полезно для меня. Спасибо - person sashas; 17.03.2019
comment
@sashas Я был неправ - это возможно, только если вы перейдете к работе с ObjectMapper напрямую. См., например. этот комментарий. - person Travis Brown; 17.03.2019
comment
@sashas О, и этот пример кода в комментарии кажется устаревшим, так что даже это не вариант. Я думаю, вы застряли с системным свойством. - person Travis Brown; 17.03.2019