Skip to content

Close #1121 - Improve numeric values support - #1422

Open
cchantep wants to merge 1 commit into
playframework:mainfrom
cchantep:feature/1121_number
Open

cchantep wants to merge 1 commit into
playframework:mainfrom
cchantep:feature/1121_number

Conversation

@cchantep

@cchantep cchantep commented Sep 12, 2026

Copy link
Copy Markdown
Member

Pull Request Checklist

  • Have you read through the contributor guidelines?
  • Have you squashed your commits?
  • Have you added copyright headers to new files?
  • Have you updated the documentation?
  • Have you added tests for any changed functionality?

Purpose

Improve performance about the JSON numbers for the following use cases:

  • parsing/deserialization
  • in-memory instantiation
  • validation
  • serialization

Background Context

The 2 main points of the design are described below.

  • Laziness: until it's evaluated (directly accessing the JsNumber.value) or validated (using Reads), a deserialized JsNumber only kept the raw representation from the JSON parser, without more processing/conversion.
  • Specialization: Refactor JsNumber as a sealed family with different specifialized implementation class according to use cases and underlying value precision/representation, allow to implement feature efficiently according to each one.
JsValue
 |
 +- JsNumber
      |
      +- JsLazy (only from parsing)
      +- JsBigDecimal
      +- JsBigInteger
      +- JsLong
      +- JsNumericInt (Byte, Short, Int)
      +- JsNumericFloat (Float, Double)

All the implementations,

  • are mutually comparable (consistent equals/hashCode),
  • transparent/invisible, only JsNumber is visible in the API, to stay compatible.

Benchmarks

New benchmarks have been added in benchmarks/src/main/scala/play/api/libs/json/JsonNumberBench.scala to measure and compare the performance of different kinds of JSON numbers across the following use cases.

Parse

Basically Json.parse(x).asInstanceOf[JsNumber], where x is a JSON raw number (integer, float, double, big decimal, ...).

parse

Stable scores for BigInteger, Decimal and LargeDecimal, with significant improvements for Int, Long and SmallInteger.

Eval parsed

Accessing the value: BigDecimal field of a parsed JsNumber; basically Json.parse(x).asInstanceOf[JsNumber].value.

evalParsed

Stable scores for BigInteger and LargeDecimal, degradation for Decimal (float or double), and improvements for Int, Long and SmallInteger.

Instantiation

The JsNumber(..) factories, creating JsNumber instances using the specialized implementation classes.

jsNumber

Overall improvements across all cases.

Validation

From JSON parsing to Reads, basically Json.parse(..).as[Int/Float/Double/BigDecimal/..].

read

Stable scores for BigDecimal, BigInt, Double and Float, with significant improvements for Byte, Short, Int and Long.

Write

Using Writes matching the specialized implementations; basically Json.toJson(..).

write

Overall improvements across all cases, except for the stable score for Byte.

Serialization

Basically Json.toJson(aJsNumber).

serialize

Stable scores for BigDecimal, BigInt and LargeBigDecimal, with improvements for the other cases.

Overall

The specialized JsNumber implementations provide noticeable performance improvements for several numeric types, particularly Byte, Short, Int, Long and SmallInteger. The other numeric representations remain broadly stable, with some trade-offs depending on the operation and representation.

The benchmarks show improvements across most operations, while also highlighting a regression for Decimal when evaluating its BigDecimal representation.

Overall, the changes improve performance for the common integer cases while keeping the impact on the other numeric types limited.

References

#1121

@cchantep cchantep changed the title Close #1121 - Improve numeric values support Draft: Close #1121 - Improve numeric values support Sep 12, 2026
@cchantep
cchantep force-pushed the feature/1121_number branch 5 times, most recently from 6bb28c0 to db3166a Compare September 13, 2026 20:32

debug(showCode(generated))

c.Expr[Reads[T]](c.typeCheck(generated))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecated

override def serialize(value: JsValue, json: JsonGenerator, provider: SerializerProvider): Unit = {
value match {
case JsNumber(v) => {
case n: JsNumber.JsLazy =>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specialize

"num0.5" in assertSizes("""0.5""", 80, 80)
"numLongMax" in assertSizes(Long.MaxValue.toString, 144, 144)
"numDoubleMax" in assertSizes(Double.MaxValue.toString, 144, 144)
"num0" in assertSizes("""0""", 32, 80)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update according to size changes due to specific implementation classes


parsed.mustEqual(json)

json.mustEqual(parsed)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check consistency

val exceedsScaleLimit = BigDecimal(2, parserSettings.bigDecimalParseConfig.scaleLimit + 1)
Json
.parse(bigNumbersJson(bigDec = exceedsScaleLimit.toString))
.as[BigNumbers]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception thrown as soon as .parse

object JsonSpec {
val exceedsDigitsLimit: BigDecimal = BigDecimal("9" * 1000000)
lazy val exceedsDigitsLimit: BigDecimal = {
// BigDecimal("9" * 1000000)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such instantiation was really too slow


package play.api.libs.json

private[json] trait JsNumberExtractors { self: JsNumber.type =>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extractors used by Reads according JsNumber implementations

* Represent a Json number value.
*/
case class JsNumber(value: BigDecimal) extends JsValue
sealed trait JsNumber extends JsValue with Serializable {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor as sealed family

/**
* Serializer for Java BigDecimal types.
*/
implicit object JavaBigDecimalWrites extends Writes[java.math.BigDecimal] {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Writes

@cchantep
cchantep marked this pull request as ready for review September 13, 2026 20:43
@cchantep cchantep changed the title Draft: Close #1121 - Improve numeric values support Close #1121 - Improve numeric values support Sep 13, 2026
@cchantep
cchantep force-pushed the feature/1121_number branch 3 times, most recently from a5d054e to 58953ab Compare September 15, 2026 14:43
@cchantep

Copy link
Copy Markdown
Member Author

Note that if update to Jackson 3, the JsonParser would directly provides distinction between the decimal, with the new JsonParser.getNumberTypeFP with returns a dedicated JsonParser.NumberTypeFP enum:

UNKNOWN (default for untyped JSON floating numbers, unless parsed into exact representations)

FLOAT16 (IEEE 754 half-precision)

FLOAT32 (Float)

DOUBLE64 (Double)

BIG_DECIMAL (BigDecimal)

That would possibly make the JacksonJson mapper much more efficient.

@cchantep
cchantep force-pushed the feature/1121_number branch 17 times, most recently from 077f285 to defaa3c Compare September 16, 2026 16:40
@cchantep
cchantep force-pushed the feature/1121_number branch 2 times, most recently from 70b83d3 to 07cb4f4 Compare September 16, 2026 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant