Skip to content

feat: transientNull option support #1307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ final class stringified extends StaticAnnotation
* arrays or collections (turned on by default)
* @param transientNone a flag that turns on skipping serialization of fields that have empty values of
* options (turned on by default)
* @param transientNull a flag that turns on skipping serialization of fields that have null values of
* objects (turned off by default)
* @param requireCollectionFields a flag that turn on checking of presence of collection fields and forces
* serialization when they are empty
* @param bigDecimalPrecision a precision in 'BigDecimal' values (34 by default that is a precision for decimal128,
Expand Down Expand Up @@ -113,6 +115,7 @@ class CodecMakerConfig private[macros] (
val transientDefault: Boolean,
val transientEmpty: Boolean,
val transientNone: Boolean,
val transientNull: Boolean,
val requireCollectionFields: Boolean,
val bigDecimalPrecision: Int,
val bigDecimalScaleLimit: Int,
Expand Down Expand Up @@ -161,6 +164,8 @@ class CodecMakerConfig private[macros] (

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

def withTransientNull(transientNull: Boolean): CodecMakerConfig = copy(transientNull = transientNull)

def withRequireCollectionFields(requireCollectionFields: Boolean): CodecMakerConfig =
copy(requireCollectionFields = requireCollectionFields)

Expand Down Expand Up @@ -224,6 +229,7 @@ class CodecMakerConfig private[macros] (
transientDefault: Boolean = transientDefault,
transientEmpty: Boolean = transientEmpty,
transientNone: Boolean = transientNone,
transientNull: Boolean = transientNull,
requireCollectionFields: Boolean = requireCollectionFields,
bigDecimalPrecision: Int = bigDecimalPrecision,
bigDecimalScaleLimit: Int = bigDecimalScaleLimit,
Expand Down Expand Up @@ -255,6 +261,7 @@ class CodecMakerConfig private[macros] (
transientDefault = transientDefault,
transientEmpty = transientEmpty,
transientNone = transientNone,
transientNull = transientNull,
requireCollectionFields = requireCollectionFields,
bigDecimalPrecision = bigDecimalPrecision,
bigDecimalScaleLimit = bigDecimalScaleLimit,
Expand Down Expand Up @@ -288,6 +295,7 @@ object CodecMakerConfig extends CodecMakerConfig(
transientDefault = true,
transientEmpty = true,
transientNone = true,
transientNull = false,
requireCollectionFields = false,
bigDecimalPrecision = 34,
bigDecimalScaleLimit = 6178,
Expand Down Expand Up @@ -340,6 +348,7 @@ object CodecMakerConfig extends CodecMakerConfig(
case '{ ($x: CodecMakerConfig).withTransientDefault($v) } => Some(x.valueOrAbort.withTransientDefault(v.valueOrAbort))
case '{ ($x: CodecMakerConfig).withTransientEmpty($v) } => Some(x.valueOrAbort.withTransientEmpty(v.valueOrAbort))
case '{ ($x: CodecMakerConfig).withTransientNone($v) } => Some(x.valueOrAbort.withTransientNone(v.valueOrAbort))
case '{ ($x: CodecMakerConfig).withTransientNull($v) } => Some(x.valueOrAbort.withTransientNull(v.valueOrAbort))
case '{ ($x: CodecMakerConfig).withRequireCollectionFields($v) } => Some(x.valueOrAbort.withRequireCollectionFields(v.valueOrAbort))
case '{ ($x: CodecMakerConfig).withRequireDefaultFields($v) } => Some(x.valueOrAbort.withRequireDefaultFields(v.valueOrAbort))
case '{ ($x: CodecMakerConfig).withScalaTransientSupport($v) } => Some(x.valueOrAbort.withScalaTransientSupport(v.valueOrAbort))
Expand Down Expand Up @@ -677,6 +686,7 @@ object JsonCodecMaker {
transientDefault = false,
transientEmpty = false,
transientNone = false,
transientNull = true,
requireCollectionFields = false,
bigDecimalPrecision = 34,
bigDecimalScaleLimit = 6178,
Expand Down Expand Up @@ -794,6 +804,11 @@ object JsonCodecMaker {
def isOption(tpe: TypeRepr, types: List[TypeRepr]): Boolean = tpe <:< TypeRepr.of[Option[_]] &&
(cfg.skipNestedOptionValues || !types.headOption.exists(_ <:< TypeRepr.of[Option[_]]))

def isNullable(tpe: TypeRepr): Boolean = tpe match {
case OrType(left, right) => isNullable(right) || isNullable(left)
case _ => tpe =:= TypeRepr.of[Null]
}

def isIArray(tpe: TypeRepr): Boolean = tpe.typeSymbol.fullName == "scala.IArray$package$.IArray"

def isCollection(tpe: TypeRepr): Boolean = tpe <:< TypeRepr.of[Iterable[_]] || tpe <:< TypeRepr.of[Iterator[_]] ||
Expand Down Expand Up @@ -1945,7 +1960,7 @@ object JsonCodecMaker {
else mappedNames
})
val required = fields.collect {
case fieldInfo if !((!cfg.requireDefaultFields && fieldInfo.symbol.flags.is(Flags.HasDefault)) || isOption(fieldInfo.resolvedTpe, types) ||
case fieldInfo if !((!cfg.requireDefaultFields && fieldInfo.symbol.flags.is(Flags.HasDefault)) || isOption(fieldInfo.resolvedTpe, types) || isNullable(fieldInfo.resolvedTpe) ||
(!cfg.requireCollectionFields && isCollection(fieldInfo.resolvedTpe))) => fieldInfo.mappedName
}.toSet
val paramVarNum = fields.size
Expand Down Expand Up @@ -2692,6 +2707,12 @@ object JsonCodecMaker {
${genWriteVal('{v.get}, tpe1 :: fTpe :: types, fieldInfo.isStringified, None, out)}
}
}
} else if (cfg.transientNull && isNullable(fTpe)) '{
val v = ${Select(x.asTerm, fieldInfo.getterOrField).asExprOf[ft]}
if ((v != null) && v != ${d.asExprOf[ft]}) {
${genWriteConstantKey(fieldInfo.mappedName, out)}
${genWriteVal('v, fTpe :: types, fieldInfo.isStringified, None, out)}
}
} else if (fTpe <:< TypeRepr.of[Array[_]]) {
def cond(v: Expr[Array[_]])(using Quotes): Expr[Boolean] =
val da = d.asExprOf[Array[_]]
Expand Down Expand Up @@ -2756,6 +2777,12 @@ object JsonCodecMaker {
${genWriteVal('{ v.get }, tpe1 :: fTpe :: types, fieldInfo.isStringified, None, out)}
}
}
} else if (cfg.transientNull && isNullable(fTpe)) '{
val v = ${Select(x.asTerm, fieldInfo.getterOrField).asExprOf[ft]}
if (v != null) {
${genWriteConstantKey(fieldInfo.mappedName, out)}
${genWriteVal('v, fTpe :: types, fieldInfo.isStringified, None, out)}
}
} else if (cfg.transientEmpty && fTpe <:< TypeRepr.of[Array[_]]) '{
val v = ${Select(x.asTerm, fieldInfo.getterOrField).asExprOf[ft & Array[_]]}
if (v.length != 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.github.plokhotnyuk.jsoniter_scala.macros

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker._

case class NullableProperty(a: Int | Null)

given nullableValueCodec: JsonValueCodec[Int | Null] = new JsonValueCodec[Int | Null] {
def decodeValue(in: JsonReader, default: Int | Null): Int | Null = {
if (in.isNextToken('n')) {
in.rollbackToken()
in.readRawValAsBytes()
null
} else {
in.rollbackToken()
in.readInt()
}
}

def encodeValue(x: Int | Null, out: JsonWriter): Unit = {
if (x == null) {
out.writeNull()
} else {
out.writeVal(x.asInstanceOf[Int])
}
}

val nullValue: Int | Null = null
}

// (CodecMakerConfig.withDiscriminatorFieldName(None))
class JsonCodecMakerNullableSpec extends VerifyingSpec {
"JsonCodecMaker.make generate codecs which" should {
"serialize and deserialize case class with nullable values (default behavior)" in {
verifySerDeser(make[NullableProperty], NullableProperty(a = null),
"""{"a":null}""")
}
"serialize and deserialize case class with nullable values (transient null behavior)" in {
verifySerDeser(make[NullableProperty](CodecMakerConfig.withTransientNull(true)), NullableProperty(a = null),
"""{}""")
}
}
}
2 changes: 1 addition & 1 deletion version.sbt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ThisBuild / version := "2.36.8-SNAPSHOT"
ThisBuild / version := "2.36.9-SNAPSHOT"
Loading