Skip to content

Commit fbdd283

Browse files
Dolfik1Nikolay
andauthored
Add transientNull compile-time option (#1307)
Co-authored-by: Nikolay <[email protected]>
1 parent 1e8c1f3 commit fbdd283

File tree

3 files changed

+72
-2
lines changed

3 files changed

+72
-2
lines changed

jsoniter-scala-macros/shared/src/main/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMaker.scala

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ final class stringified extends StaticAnnotation
5656
* arrays or collections (turned on by default)
5757
* @param transientNone a flag that turns on skipping serialization of fields that have empty values of
5858
* options (turned on by default)
59+
* @param transientNull a flag that turns on skipping serialization of fields that have null values of
60+
* objects (turned off by default)
5961
* @param requireCollectionFields a flag that turn on checking of presence of collection fields and forces
6062
* serialization when they are empty
6163
* @param bigDecimalPrecision a precision in 'BigDecimal' values (34 by default that is a precision for decimal128,
@@ -113,6 +115,7 @@ class CodecMakerConfig private[macros] (
113115
val transientDefault: Boolean,
114116
val transientEmpty: Boolean,
115117
val transientNone: Boolean,
118+
val transientNull: Boolean,
116119
val requireCollectionFields: Boolean,
117120
val bigDecimalPrecision: Int,
118121
val bigDecimalScaleLimit: Int,
@@ -161,6 +164,8 @@ class CodecMakerConfig private[macros] (
161164

162165
def withTransientNone(transientNone: Boolean): CodecMakerConfig = copy(transientNone = transientNone)
163166

167+
def withTransientNull(transientNull: Boolean): CodecMakerConfig = copy(transientNull = transientNull)
168+
164169
def withRequireCollectionFields(requireCollectionFields: Boolean): CodecMakerConfig =
165170
copy(requireCollectionFields = requireCollectionFields)
166171

@@ -224,6 +229,7 @@ class CodecMakerConfig private[macros] (
224229
transientDefault: Boolean = transientDefault,
225230
transientEmpty: Boolean = transientEmpty,
226231
transientNone: Boolean = transientNone,
232+
transientNull: Boolean = transientNull,
227233
requireCollectionFields: Boolean = requireCollectionFields,
228234
bigDecimalPrecision: Int = bigDecimalPrecision,
229235
bigDecimalScaleLimit: Int = bigDecimalScaleLimit,
@@ -255,6 +261,7 @@ class CodecMakerConfig private[macros] (
255261
transientDefault = transientDefault,
256262
transientEmpty = transientEmpty,
257263
transientNone = transientNone,
264+
transientNull = transientNull,
258265
requireCollectionFields = requireCollectionFields,
259266
bigDecimalPrecision = bigDecimalPrecision,
260267
bigDecimalScaleLimit = bigDecimalScaleLimit,
@@ -288,6 +295,7 @@ object CodecMakerConfig extends CodecMakerConfig(
288295
transientDefault = true,
289296
transientEmpty = true,
290297
transientNone = true,
298+
transientNull = false,
291299
requireCollectionFields = false,
292300
bigDecimalPrecision = 34,
293301
bigDecimalScaleLimit = 6178,
@@ -340,6 +348,7 @@ object CodecMakerConfig extends CodecMakerConfig(
340348
case '{ ($x: CodecMakerConfig).withTransientDefault($v) } => Some(x.valueOrAbort.withTransientDefault(v.valueOrAbort))
341349
case '{ ($x: CodecMakerConfig).withTransientEmpty($v) } => Some(x.valueOrAbort.withTransientEmpty(v.valueOrAbort))
342350
case '{ ($x: CodecMakerConfig).withTransientNone($v) } => Some(x.valueOrAbort.withTransientNone(v.valueOrAbort))
351+
case '{ ($x: CodecMakerConfig).withTransientNull($v) } => Some(x.valueOrAbort.withTransientNull(v.valueOrAbort))
343352
case '{ ($x: CodecMakerConfig).withRequireCollectionFields($v) } => Some(x.valueOrAbort.withRequireCollectionFields(v.valueOrAbort))
344353
case '{ ($x: CodecMakerConfig).withRequireDefaultFields($v) } => Some(x.valueOrAbort.withRequireDefaultFields(v.valueOrAbort))
345354
case '{ ($x: CodecMakerConfig).withScalaTransientSupport($v) } => Some(x.valueOrAbort.withScalaTransientSupport(v.valueOrAbort))
@@ -677,6 +686,7 @@ object JsonCodecMaker {
677686
transientDefault = false,
678687
transientEmpty = false,
679688
transientNone = false,
689+
transientNull = true,
680690
requireCollectionFields = false,
681691
bigDecimalPrecision = 34,
682692
bigDecimalScaleLimit = 6178,
@@ -794,6 +804,11 @@ object JsonCodecMaker {
794804
def isOption(tpe: TypeRepr, types: List[TypeRepr]): Boolean = tpe <:< TypeRepr.of[Option[_]] &&
795805
(cfg.skipNestedOptionValues || !types.headOption.exists(_ <:< TypeRepr.of[Option[_]]))
796806

807+
def isNullable(tpe: TypeRepr): Boolean = tpe match {
808+
case OrType(left, right) => isNullable(right) || isNullable(left)
809+
case _ => tpe =:= TypeRepr.of[Null]
810+
}
811+
797812
def isIArray(tpe: TypeRepr): Boolean = tpe.typeSymbol.fullName == "scala.IArray$package$.IArray"
798813

799814
def isCollection(tpe: TypeRepr): Boolean = tpe <:< TypeRepr.of[Iterable[_]] || tpe <:< TypeRepr.of[Iterator[_]] ||
@@ -1945,7 +1960,7 @@ object JsonCodecMaker {
19451960
else mappedNames
19461961
})
19471962
val required = fields.collect {
1948-
case fieldInfo if !((!cfg.requireDefaultFields && fieldInfo.symbol.flags.is(Flags.HasDefault)) || isOption(fieldInfo.resolvedTpe, types) ||
1963+
case fieldInfo if !((!cfg.requireDefaultFields && fieldInfo.symbol.flags.is(Flags.HasDefault)) || isOption(fieldInfo.resolvedTpe, types) || isNullable(fieldInfo.resolvedTpe) ||
19491964
(!cfg.requireCollectionFields && isCollection(fieldInfo.resolvedTpe))) => fieldInfo.mappedName
19501965
}.toSet
19511966
val paramVarNum = fields.size
@@ -2692,6 +2707,12 @@ object JsonCodecMaker {
26922707
${genWriteVal('{v.get}, tpe1 :: fTpe :: types, fieldInfo.isStringified, None, out)}
26932708
}
26942709
}
2710+
} else if (cfg.transientNull && isNullable(fTpe)) '{
2711+
val v = ${Select(x.asTerm, fieldInfo.getterOrField).asExprOf[ft]}
2712+
if ((v != null) && v != ${d.asExprOf[ft]}) {
2713+
${genWriteConstantKey(fieldInfo.mappedName, out)}
2714+
${genWriteVal('v, fTpe :: types, fieldInfo.isStringified, None, out)}
2715+
}
26952716
} else if (fTpe <:< TypeRepr.of[Array[_]]) {
26962717
def cond(v: Expr[Array[_]])(using Quotes): Expr[Boolean] =
26972718
val da = d.asExprOf[Array[_]]
@@ -2756,6 +2777,12 @@ object JsonCodecMaker {
27562777
${genWriteVal('{ v.get }, tpe1 :: fTpe :: types, fieldInfo.isStringified, None, out)}
27572778
}
27582779
}
2780+
} else if (cfg.transientNull && isNullable(fTpe)) '{
2781+
val v = ${Select(x.asTerm, fieldInfo.getterOrField).asExprOf[ft]}
2782+
if (v != null) {
2783+
${genWriteConstantKey(fieldInfo.mappedName, out)}
2784+
${genWriteVal('v, fTpe :: types, fieldInfo.isStringified, None, out)}
2785+
}
27592786
} else if (cfg.transientEmpty && fTpe <:< TypeRepr.of[Array[_]]) '{
27602787
val v = ${Select(x.asTerm, fieldInfo.getterOrField).asExprOf[ft & Array[_]]}
27612788
if (v.length != 0) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.github.plokhotnyuk.jsoniter_scala.macros
2+
3+
import com.github.plokhotnyuk.jsoniter_scala.core._
4+
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker._
5+
6+
case class NullableProperty(a: Int | Null)
7+
8+
given nullableValueCodec: JsonValueCodec[Int | Null] = new JsonValueCodec[Int | Null] {
9+
def decodeValue(in: JsonReader, default: Int | Null): Int | Null = {
10+
if (in.isNextToken('n')) {
11+
in.rollbackToken()
12+
in.readRawValAsBytes()
13+
null
14+
} else {
15+
in.rollbackToken()
16+
in.readInt()
17+
}
18+
}
19+
20+
def encodeValue(x: Int | Null, out: JsonWriter): Unit = {
21+
if (x == null) {
22+
out.writeNull()
23+
} else {
24+
out.writeVal(x.asInstanceOf[Int])
25+
}
26+
}
27+
28+
val nullValue: Int | Null = null
29+
}
30+
31+
// (CodecMakerConfig.withDiscriminatorFieldName(None))
32+
class JsonCodecMakerNullableSpec extends VerifyingSpec {
33+
"JsonCodecMaker.make generate codecs which" should {
34+
"serialize and deserialize case class with nullable values (default behavior)" in {
35+
verifySerDeser(make[NullableProperty], NullableProperty(a = null),
36+
"""{"a":null}""")
37+
}
38+
"serialize and deserialize case class with nullable values (transient null behavior)" in {
39+
verifySerDeser(make[NullableProperty](CodecMakerConfig.withTransientNull(true)), NullableProperty(a = null),
40+
"""{}""")
41+
}
42+
}
43+
}

version.sbt

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

0 commit comments

Comments
 (0)