Skip to content

Commit 2bcbeed

Browse files
authored
Add an ability to customize number serializer for circe integration (#1113)
1 parent 3c15676 commit 2bcbeed

File tree

4 files changed

+77
-14
lines changed

4 files changed

+77
-14
lines changed

jsoniter-scala-circe/shared/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/circe/JsoniterScalaCodec.scala

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.github.plokhotnyuk.jsoniter_scala.circe
22

3-
import com.github.plokhotnyuk.jsoniter_scala.core.{JsonReader, JsonValueCodec}
3+
import com.github.plokhotnyuk.jsoniter_scala.core.{JsonReader, JsonValueCodec, JsonWriter}
44
import io.circe._
55

66
object JsoniterScalaCodec {
@@ -20,10 +20,28 @@ object JsoniterScalaCodec {
2020
* @param numberParser a function that parses JSON numbers
2121
* @return The JSON codec
2222
*/
23+
def jsonCodec(
24+
maxDepth: Int,
25+
initialSize: Int,
26+
doSerialize: Json => Boolean,
27+
numberParser: JsonReader => Json): JsonValueCodec[Json] =
28+
jsonCodec(maxDepth, initialSize, doSerialize, numberParser, io.circe.JsoniterScalaCodec.defaultNumberSerializer)
29+
30+
/**
31+
* Creates a JSON value codec that parses and serialize to/from circe's JSON AST.
32+
*
33+
* @param maxDepth the maximum depth for decoding
34+
* @param initialSize the initial size hint for object and array collections
35+
* @param doSerialize a predicate that determines whether a value should be serialized
36+
* @param numberParser a function that parses JSON numbers
37+
* @param numberSerializer a routine that serializes JSON numbers
38+
* @return The JSON codec
39+
*/
2340
def jsonCodec(
2441
maxDepth: Int = 128,
2542
initialSize: Int = 8,
2643
doSerialize: Json => Boolean = _ => true,
27-
numberParser: JsonReader => Json = io.circe.JsoniterScalaCodec.defaultNumberParser): JsonValueCodec[Json] =
28-
new io.circe.JsoniterScalaCodec(maxDepth, initialSize, doSerialize, numberParser)
44+
numberParser: JsonReader => Json = io.circe.JsoniterScalaCodec.defaultNumberParser,
45+
numberSerializer: (JsonWriter, JsonNumber) => Unit = io.circe.JsoniterScalaCodec.defaultNumberSerializer): JsonValueCodec[Json] =
46+
new io.circe.JsoniterScalaCodec(maxDepth, initialSize, doSerialize, numberParser, numberSerializer)
2947
}

jsoniter-scala-circe/shared/src/main/scala/io/circe/JsoniterScalaCodec.scala

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,39 @@ object JsoniterScalaCodec {
3535
} else new JsonBigDecimal(in.readBigDecimal(null).bigDecimal)
3636
})
3737

38+
val defaultNumberSerializer: (JsonWriter, JsonNumber) => Unit = (out: JsonWriter, x: JsonNumber) => x match {
39+
case l: JsonLong => out.writeVal(l.value)
40+
case f: JsonFloat => out.writeVal(f.value)
41+
case d: JsonDouble => out.writeVal(d.value)
42+
case bd: JsonBigDecimal => out.writeVal(bd.value)
43+
case _ => out.writeRawVal(x.toString.getBytes(StandardCharsets.UTF_8))
44+
}
45+
46+
val jsCompatibleNumberSerializer: (JsonWriter, JsonNumber) => Unit = (out: JsonWriter, x: JsonNumber) => x match {
47+
case l: JsonLong =>
48+
val v = l.value
49+
if (v >= -4503599627370496L && v < 4503599627370496L) out.writeVal(v)
50+
else out.writeValAsString(v)
51+
case f: JsonFloat => out.writeVal(f.value)
52+
case d: JsonDouble => out.writeVal(d.value)
53+
case bd: JsonBigDecimal =>
54+
val v = bd.value
55+
val bl = v.unscaledValue.bitLength
56+
val s = v.scale
57+
if (bl <= 52 && s >= -256 && s <= 256) out.writeVal(v)
58+
else out.writeValAsString(v)
59+
case _ => x.toBigDecimal match {
60+
case Some(bd) =>
61+
val u = bd.underlying
62+
val bl = u.unscaledValue.bitLength
63+
val s = u.scale
64+
if (bl <= 52 && s >= -256 && s <= 256) out.writeVal(u)
65+
else out.writeValAsString(u)
66+
case _ =>
67+
out.writeVal(x.toString)
68+
}
69+
}
70+
3871
/**
3972
* Converts an ASCII byte array to a JSON string.
4073
*
@@ -94,13 +127,27 @@ object JsoniterScalaCodec {
94127
* @param initialSize the initial size hint for object and array collections
95128
* @param doSerialize a predicate that determines whether a value should be serialized
96129
* @param numberParser a function that parses JSON numbers
130+
* @param numberSerializer a function that serializes JSON numbers
97131
* @return The JSON codec
98132
*/
99133
final class JsoniterScalaCodec(
100134
maxDepth: Int,
101135
initialSize: Int,
102136
doSerialize: Json => Boolean,
103-
numberParser: JsonReader => Json) extends JsonValueCodec[Json] {
137+
numberParser: JsonReader => Json,
138+
numberSerializer: (JsonWriter, JsonNumber) => Unit) extends JsonValueCodec[Json] {
139+
140+
/**
141+
* An auxiliary constructor for backward binary compatibility.
142+
*
143+
* @param maxDepth the maximum depth for decoding
144+
* @param initialSize the initial size hint for object and array collections
145+
* @param doSerialize a predicate that determines whether a value should be serialized
146+
* @param numberParser a function that parses JSON numbers
147+
*/
148+
def this(maxDepth: Int, initialSize: Int, doSerialize: Json => Boolean, numberParser: JsonReader => Json) =
149+
this(maxDepth, initialSize, doSerialize, numberParser, JsoniterScalaCodec.defaultNumberSerializer)
150+
104151
private[this] val trueValue = True
105152
private[this] val falseValue = False
106153
private[this] val emptyArrayValue = new JArray(Vector.empty)
@@ -161,7 +208,7 @@ final class JsoniterScalaCodec(
161208
if (str.length != 1) out.writeVal(str)
162209
else out.writeVal(str.charAt(0))
163210
case b: JBoolean => out.writeVal(b.value)
164-
case n: JNumber => encodeJsonNumber(n.value, out)
211+
case n: JNumber => numberSerializer(out, n.value)
165212
case a: JArray =>
166213
val depthM1 = depth - 1
167214
if (depthM1 < 0) out.encodeError("depth limit exceeded")
@@ -183,12 +230,4 @@ final class JsoniterScalaCodec(
183230
out.writeObjectEnd()
184231
case _ => out.writeNull()
185232
}
186-
187-
private[this] def encodeJsonNumber(x: JsonNumber, out: JsonWriter): Unit = x match {
188-
case l: JsonLong => out.writeVal(l.value)
189-
case f: JsonFloat => out.writeVal(f.value)
190-
case d: JsonDouble => out.writeVal(d.value)
191-
case bd: JsonBigDecimal => out.writeVal(bd.value)
192-
case _ => out.writeRawVal(x.toString.getBytes(StandardCharsets.UTF_8))
193-
}
194233
}

jsoniter-scala-circe/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/circe/JsoniterScalaCodecSpec.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ class JsoniterScalaCodecSpec extends AnyWordSpec with Matchers {
3535
val json = readFromString(jsonStr)
3636
writeToString(json) shouldBe jsonStr
3737
}
38+
"allow customization for number serialization" in {
39+
val codec = jsonCodec(numberSerializer = io.circe.JsoniterScalaCodec.jsCompatibleNumberSerializer)
40+
val jsonStr = """{"s":"VVV","n1":1.0,"n2":4503599627370497,"a":[null,"WWW",[],{}],"o":{"a":4e+297}}"""
41+
val json = readFromString(jsonStr)
42+
writeToString(json)(codec) shouldBe """{"s":"VVV","n1":1.0,"n2":"4503599627370497","a":[null,"WWW",[],{}],"o":{"a":"4E+297"}}"""
43+
}
3844
"not serialize invalid json" in {
3945
val json1 = parse("\"\ud800\"").getOrElse(null)
4046
assert(intercept[Throwable](writeToString(json1)).getMessage.contains("illegal char sequence of surrogate pair"))

version.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ThisBuild / version := "2.27.8-SNAPSHOT"
1+
ThisBuild / version := "2.28.0-SNAPSHOT"

0 commit comments

Comments
 (0)