Skip to content

Commit

Permalink
feat: explicitEmptyCollections for more json
Browse files Browse the repository at this point in the history
  • Loading branch information
ThijsBroersen committed Jan 23, 2025
1 parent 8d140c3 commit c86020f
Show file tree
Hide file tree
Showing 7 changed files with 1,604 additions and 387 deletions.
188 changes: 142 additions & 46 deletions zio-json/shared/src/main/scala-2.x/zio/json/macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ final case class jsonField(name: String) extends Annotation
*/
final case class jsonAliases(alias: String, aliases: String*) extends Annotation

final class jsonExplicitNull extends Annotation

/**
* Empty option fields will be encoded as `null`.
* When disabled keys with empty collections will be omitted from the JSON.
*/
final class jsonExplicitNull extends Annotation
final case class jsonExplicitEmptyCollection(enabled: Boolean = true) extends Annotation

/**
* If used on a sealed class, will determine the name of the field for disambiguating classes.
Expand Down Expand Up @@ -211,7 +213,8 @@ object DeriveJsonDecoder {
}.isDefined || !config.allowExtraFields

if (ctx.parameters.isEmpty)
new JsonDecoder[A] {
new CollectionJsonDecoder[A] {
override def unsafeDecodeMissing(trace: List[JsonError]): A = ctx.rawConstruct(Nil)

def unsafeDecode(trace: List[JsonError], in: RetractReader): A = {
if (no_extra) {
Expand All @@ -231,7 +234,7 @@ object DeriveJsonDecoder {
}
}
else
new JsonDecoder[A] {
new CollectionJsonDecoder[A] {
private[this] val (names, aliases): (Array[String], Array[(String, Int)]) = {
val names = new Array[String](ctx.parameters.size)
val aliasesBuilder = Array.newBuilder[(String, Int)]
Expand Down Expand Up @@ -259,14 +262,55 @@ object DeriveJsonDecoder {
}
(names, aliases)
}
private[this] val len = names.length
private[this] val matrix = new StringMatrix(names, aliases)
private[this] val spans = names.map(JsonError.ObjectAccess)
private[this] lazy val defaults = ctx.parameters.map(_.evaluateDefault).toArray
private[this] val len = names.length
private[this] val matrix = new StringMatrix(names, aliases)
private[this] val spans = names.map(JsonError.ObjectAccess)
private[this] val defaults = ctx.parameters.map(_.evaluateDefault).toArray
private[this] lazy val tcs =
ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]]
private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap

// private[this] val explicitNulls =
// config.explicitNulls || ctx.annotations.collectFirst { case jsonExplicitNull => () }.isDefined
private[this] val explicitEmptyCollections =
ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) =>
enabled
}.getOrElse(config.explicitEmptyCollections)

private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match {
case d: CollectionJsonDecoder[_] if !explicitEmptyCollections => true
// case d: OptionJsonDecoder[_] if !explicitNulls => true
case d: OptionJsonDecoder[_] => true
case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying)
case d => false
}
private[this] val missingValueDecoder =
if (!explicitEmptyCollections)
(idx: Int, trace: List[JsonError]) => tcs(idx).unsafeDecodeMissing(spans(idx) :: trace)
else {
lazy val missingValueDecoderMap =
tcs.map(d => if (allowMissingValueDecoder(d)) Some(d) else None).toIndexedSeq
(idx: Int, trace: List[JsonError]) =>
missingValueDecoderMap(idx)
.map(_.unsafeDecodeMissing(spans(idx) :: trace))
.getOrElse(Lexer.error("missing", spans(idx) :: trace))
}

override def unsafeDecodeMissing(trace: List[JsonError]): A = {
val ps = new Array[Any](len)
var idx = 0
while (idx < len) {
if (ps(idx) == null) {
val default = defaults(idx)
ps(idx) =
if (default ne None) default.get
else missingValueDecoder(idx, trace)
}
idx += 1
}
ctx.rawConstruct(new ArraySeq(ps))
}

override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = {
Lexer.char(trace, in, '{')

Expand Down Expand Up @@ -302,7 +346,7 @@ object DeriveJsonDecoder {
val default = defaults(idx)
ps(idx) =
if (default ne None) default.get()
else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace)
else missingValueDecoder(idx, trace)
}
idx += 1
}
Expand Down Expand Up @@ -332,7 +376,7 @@ object DeriveJsonDecoder {
val default = defaults(idx)
ps(idx) =
if (default ne None) default.get()
else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace)
else missingValueDecoder(idx, trace)
}
idx += 1
}
Expand Down Expand Up @@ -433,6 +477,8 @@ object DeriveJsonEncoder {
if (ctx.parameters.isEmpty)
new JsonEncoder[A] {

override def isEmpty(a: A): Boolean = true

def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = out.write("{}")

override final def toJsonAST(a: A): Either[String, Json] =
Expand All @@ -449,25 +495,79 @@ object DeriveJsonEncoder {
private[this] val params = ctx.parameters
.filter(p => p.annotations.collectFirst { case _: jsonExclude => () }.isEmpty)
.toArray
private[this] val names =
params.map { p =>
p.annotations.collectFirst { case jsonField(name) =>
name
}.getOrElse(if (transformNames) nameTransform(p.label) else p.label)
}

private[this] val explicitNulls =
config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull])
private[this] lazy val fields = params.map {
var idx = 0
p =>
val field = (
p,
names(idx),
p.typeclass.asInstanceOf[JsonEncoder[Any]],
explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull])
)
idx += 1
field
private[this] val explicitEmptyCollections =
ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) =>
enabled
}.getOrElse(config.explicitEmptyCollections)

private[this] class Field[T](
val p: Param[JsonEncoder, A],
val name: String,
val encoder: JsonEncoder[T],
withExplicitNulls: Boolean,
withExplicitEmptyCollections: Boolean
) {
private[this] val _encodeOrSkip: T => (() => Unit) => Unit =
(withExplicitNulls, withExplicitEmptyCollections) match {
case (true, true) => _ => encode => encode()
case (false, false) => { t => encode =>
if (!encoder.isEmpty(t) && !encoder.isNothing(t)) encode() else ()
}
case (true, false) => { t => encode =>
if (!encoder.isEmpty(t)) encode() else ()
}
case (false, true) => { t => encode =>
if (!encoder.isNothing(t)) encode() else ()
}
}
def encodeOrSkip(t: T)(encode: () => Unit): Unit = _encodeOrSkip(t)(encode)

private[this] val _encodeOrDefault: T => (
Either[String, Chunk[(String, Json)]],
() => Either[String, Chunk[(String, Json)]]
) => Either[String, Chunk[(String, Json)]] =
(withExplicitNulls, withExplicitEmptyCollections) match {
case (true, true) => _ => (_, encode) => encode()
case (false, false) => { t => (default, encode) =>
if (!encoder.isEmpty(t) && !encoder.isNothing(t)) encode() else default
}
case (true, false) => { t => (default, encode) =>
if (!encoder.isEmpty(t)) encode() else default
}
case (false, true) => { t => (default, encode) =>
if (!encoder.isNothing(t)) encode() else default
}
}
def encodeOrDefault(t: T)(
encode: () => Either[String, Chunk[(String, Json)]],
default: Either[String, Chunk[(String, Json)]]
): Either[String, Chunk[(String, Json)]] =
_encodeOrDefault(t)(default, encode)
}

private[this] lazy val fields: Array[Field[Any]] = params.map { p =>
val name = p.annotations.collectFirst { case jsonField(name) =>
name
}.getOrElse(if (transformNames) nameTransform(p.label) else p.label)
val withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull])
val withExplicitEmptyCollections = p.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) =>
enabled
}.getOrElse(explicitEmptyCollections)
new Field(
p,
name,
p.typeclass.asInstanceOf[JsonEncoder[Any]],
withExplicitNulls = withExplicitNulls,
withExplicitEmptyCollections = withExplicitEmptyCollections
)
}

override def isEmpty(a: A): Boolean = fields.forall { field =>
val paramValue = field.p.dereference(a)
field.encoder.isEmpty(paramValue) || field.encoder.isNothing(paramValue)
}

def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = {
Expand All @@ -479,20 +579,17 @@ object DeriveJsonEncoder {
var prevFields = false // whether any fields have been written
while (idx < fields.length) {
val field = fields(idx)
val p = field._1.dereference(a)
if ({
val isNothing = field._3.isNothing(p)
!isNothing || field._4
}) {
val p = field.p.dereference(a)
field.encodeOrSkip(p) { () =>
// if we have at least one field already, we need a comma
if (prevFields) {
out.write(',')
JsonEncoder.pad(indent_, out)
}
JsonEncoder.string.unsafeEncode(field._2, indent_, out)
JsonEncoder.string.unsafeEncode(field.name, indent_, out)
if (indent.isEmpty) out.write(':')
else out.write(" : ")
field._3.unsafeEncode(p, indent_, out)
field.encoder.unsafeEncode(p, indent_, out)
prevFields = true // record that we have at least one field so far
}
idx += 1
Expand All @@ -502,18 +599,17 @@ object DeriveJsonEncoder {
}

override final def toJsonAST(a: A): Either[String, Json] =
ctx.parameters
.foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, param) =>
val name = param.annotations.collectFirst { case jsonField(name) =>
name
}.getOrElse(nameTransform(param.label))
val writeNulls = explicitNulls || param.annotations.exists(_.isInstanceOf[jsonExplicitNull])
c.flatMap { chunk =>
param.typeclass.toJsonAST(param.dereference(a)).map { value =>
if (!writeNulls && value == Json.Null) chunk
else chunk :+ name -> value
}
}
fields
.foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, field) =>
val param = field.p
val paramValue = field.p.dereference(a).asInstanceOf[param.PType]
field.encodeOrDefault(paramValue)(
() =>
c.flatMap { chunk =>
param.typeclass.toJsonAST(paramValue).map(value => chunk :+ field.name -> value)
},
c
)
}
.map(Json.Obj.apply)
}
Expand Down
Loading

0 comments on commit c86020f

Please sign in to comment.