diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecConfiguration.scala new file mode 100644 index 00000000..e5927496 --- /dev/null +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecConfiguration.scala @@ -0,0 +1,188 @@ +package zio.json + +import zio.json.JsonCodecConfiguration.SumTypeHandling +import zio.json.JsonCodecConfiguration.SumTypeHandling.WrapperWithClassNameField + +/** + * When disabled for decoding, keys with empty collections will be omitted from the JSON. When disabled for encoding, + * missing keys will default to empty collections. + */ +case class ExplicitEmptyCollections(encoding: Boolean = true, decoding: Boolean = true) + +/** + * Implicit codec derivation configuration. + * + * @param sumTypeHandling + * see [[jsonDiscriminator]] + * @param fieldNameMapping + * see [[jsonMemberNames]] + * @param allowExtraFields + * see [[jsonNoExtraFields]] + * @param sumTypeMapping + * see [[jsonHintNames]] + * @param explicitNulls + * turns on explicit serialization of optional fields with None values + * @param explicitEmptyCollections + * turns on explicit serialization of fields with empty collections + * @param enumValuesAsStrings + * turns on serialization of enum values and sealed trait's case objects as strings + */ +final case class JsonCodecConfiguration( + sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, + fieldNameMapping: JsonMemberFormat = IdentityFormat, + allowExtraFields: Boolean = true, + sumTypeMapping: JsonMemberFormat = IdentityFormat, + explicitNulls: Boolean = false, + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections(), + enumValuesAsStrings: Boolean = false +) { + def this( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = this( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + false + ) + + def this( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = this( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + ExplicitEmptyCollections(), + false + ) + + def copy( + sumTypeHandling: SumTypeHandling = WrapperWithClassNameField.asInstanceOf[SumTypeHandling], + fieldNameMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], + allowExtraFields: Boolean = true, + sumTypeMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], + explicitNulls: Boolean = false, + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections(), + enumValuesAsStrings: Boolean = false + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + enumValuesAsStrings + ) + + def copy( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + this.enumValuesAsStrings + ) + + def copy( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + this.explicitEmptyCollections, + this.enumValuesAsStrings + ) +} + +object JsonCodecConfiguration { + def apply( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + false + ) + + def apply( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + ExplicitEmptyCollections(), + false + ) + + implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() + + sealed trait SumTypeHandling { + def discriminatorField: Option[String] + } + + object SumTypeHandling { + + /** + * Use an object with a single key that is the class name. + */ + case object WrapperWithClassNameField extends SumTypeHandling { + override def discriminatorField: Option[String] = None + } + + /** + * For sealed classes, will determine the name of the field for disambiguating classes. + * + * The default is to not use a typehint field and instead have an object with a single key that is the class name. + * See [[WrapperWithClassNameField]]. + * + * Note that using a discriminator is less performant, uses more memory, and may be prone to DOS attacks that are + * impossible with the default encoding. In addition, there is slightly less type safety when using custom product + * encoders (which must write an unenforced object type). Only use this option if you must model an externally + * defined schema. + */ + final case class DiscriminatorField(name: String) extends SumTypeHandling { + override def discriminatorField: Option[String] = Some(name) + } + } +} diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index c170572e..fb29986e 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -196,6 +196,25 @@ final class jsonNoExtraFields extends Annotation */ final class jsonExclude extends Annotation +private class CaseObjectDecoder[Typeclass[_], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) + extends CollectionJsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + if (no_extra) { + Lexer.char(trace, in, '{') + Lexer.char(trace, in, '}') + } else Lexer.skipValue(trace, in) + ctx.rawConstruct(Nil) + } + + override def unsafeDecodeMissing(trace: List[JsonError]): A = ctx.rawConstruct(Nil) + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case _: Json.Obj | Json.Null => ctx.rawConstruct(Nil) + case _ => Lexer.error("expected object", trace) + } +} + object DeriveJsonDecoder { type Typeclass[A] = JsonDecoder[A] @@ -212,25 +231,7 @@ object DeriveJsonDecoder { }.isDefined || !config.allowExtraFields if (ctx.parameters.isEmpty) - new CollectionJsonDecoder[A] { - override def unsafeDecodeMissing(trace: List[JsonError]): A = ctx.rawConstruct(Nil) - - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - if (no_extra) { - Lexer.char(trace, in, '{') - Lexer.char(trace, in, '}') - } else { - Lexer.skipValue(trace, in) - } - ctx.rawConstruct(Nil) - } - - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case _: Json.Obj | Json.Null => ctx.rawConstruct(Nil) - case _ => Lexer.error("expected object", trace) - } - } + new CaseObjectDecoder(ctx, no_extra) else new CollectionJsonDecoder[A] { private[this] val (names, aliases): (Array[String], Array[(String, Int)]) = { @@ -403,10 +404,31 @@ object DeriveJsonDecoder { lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap = names.zipWithIndex.toMap - def discrim = + val isEnumeration = config.enumValuesAsStrings && + ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[JsonDecoder, _]]) + + val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - if (discrim.isEmpty) { + if (isEnumeration && discrim.isEmpty) { + new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val idx = Lexer.enumeration(trace, in, matrix) + if (idx != -1) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + else Lexer.error("invalid enumeration value", trace) + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case s: Json.Str => + namesMap.get(s.value) match { + case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case _ => Lexer.error("invalid enumeration value", trace) + } + case _ => Lexer.error("expected string", trace) + } + } + } else if (discrim.isEmpty) { // We're not allowing extra fields in this encoding new JsonDecoder[A] { private[this] val spans = names.map(JsonError.ObjectAccess) @@ -481,17 +503,19 @@ object DeriveJsonDecoder { } object DeriveJsonEncoder { + private lazy val caseObjectEncoder = new JsonEncoder[Any] { + override def isEmpty(a: Any): Boolean = true + + def unsafeEncode(a: Any, indent: Option[Int], out: Write): Unit = out.write("{}") + + override final def toJsonAST(a: Any): Either[String, Json] = new Right(Json.Obj.empty) + } + type Typeclass[A] = JsonEncoder[A] def join[A](ctx: CaseClass[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = 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] = new Right(Json.Obj.empty) - } + caseObjectEncoder.narrow[A] else new JsonEncoder[A] { private[this] val (transformNames, nameTransform): (Boolean, String => String) = @@ -584,6 +608,8 @@ object DeriveJsonEncoder { } def split[A](ctx: SealedTrait[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = { + val isEnumeration = config.enumValuesAsStrings && + ctx.subtypes.forall(_.typeclass == caseObjectEncoder) val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = ctx.subtypes.map { p => @@ -592,7 +618,17 @@ object DeriveJsonEncoder { val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - if (discrim.isEmpty) { + if (isEnumeration && discrim.isEmpty) { + new JsonEncoder[A] { + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => + JsonEncoder.string.unsafeEncode(names(sub.index), indent, out) + } + + override final def toJsonAST(a: A): Either[String, Json] = ctx.split(a) { sub => + new Right(new Json.Str(names(sub.index))) + } + } + } else if (discrim.isEmpty) { new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => out.write('{') diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecConfiguration.scala similarity index 66% rename from zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala rename to zio-json/shared/src/main/scala-3/zio/json/JsonCodecConfiguration.scala index eaa1d512..42a5b5a1 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecConfiguration.scala @@ -20,6 +20,9 @@ case class ExplicitEmptyCollections(encoding: Boolean = true, decoding: Boolean * see [[jsonNoExtraFields]] * @param sumTypeMapping * see [[jsonHintNames]] + * @param explicitNulls turns on explicit serialization of optional fields with None values + * @param explicitEmptyCollections turns on explicit serialization of fields with empty collections + * @param enumValuesAsStrings turns on serialization of enum values and sealed trait's case objects as strings */ final case class JsonCodecConfiguration( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, @@ -27,8 +30,26 @@ final case class JsonCodecConfiguration( allowExtraFields: Boolean = true, sumTypeMapping: JsonMemberFormat = IdentityFormat, explicitNulls: Boolean = false, - explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections() + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections(), + enumValuesAsStrings: Boolean = true ) { + def this( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = this( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + true + ) + def this( sumTypeHandling: SumTypeHandling, fieldNameMapping: JsonMemberFormat, @@ -41,7 +62,8 @@ final case class JsonCodecConfiguration( allowExtraFields, sumTypeMapping, explicitNulls, - ExplicitEmptyCollections() + ExplicitEmptyCollections(), + true ) def copy( @@ -50,14 +72,33 @@ final case class JsonCodecConfiguration( allowExtraFields: Boolean = true, sumTypeMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], explicitNulls: Boolean = false, - explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections() + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections(), + enumValuesAsStrings: Boolean = true ) = new JsonCodecConfiguration( sumTypeHandling, fieldNameMapping, allowExtraFields, sumTypeMapping, explicitNulls, - explicitEmptyCollections + explicitEmptyCollections, + enumValuesAsStrings + ) + + def copy( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + this.enumValuesAsStrings ) def copy( @@ -72,11 +113,29 @@ final case class JsonCodecConfiguration( allowExtraFields, sumTypeMapping, explicitNulls, - this.explicitEmptyCollections + this.explicitEmptyCollections, + this.enumValuesAsStrings ) } object JsonCodecConfiguration { + def apply( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + true + ) + def apply( sumTypeHandling: SumTypeHandling, fieldNameMapping: JsonMemberFormat, @@ -89,7 +148,8 @@ object JsonCodecConfiguration { allowExtraFields, sumTypeMapping, explicitNulls, - ExplicitEmptyCollections() + ExplicitEmptyCollections(), + true ) implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index c220a800..db41270c 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -6,7 +6,6 @@ import scala.deriving.Mirror import scala.compiletime.* import scala.reflect.* import zio.Chunk - import zio.json.JsonDecoder.JsonError import zio.json.ast.Json import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } @@ -209,7 +208,8 @@ final class jsonNoExtraFields extends Annotation */ final class jsonExclude extends Annotation -private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) extends CollectionJsonDecoder[A] { +private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) + extends CollectionJsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { if (no_extra) { Lexer.char(trace, in, '{') @@ -223,7 +223,7 @@ private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case _: Json.Obj | Json.Null => ctx.rawConstruct(Nil) - case _ => Lexer.error("expected object", trace) + case _ => Lexer.error("expected object", trace) } } @@ -236,17 +236,16 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv .map(true -> _) .getOrElse(false -> identity) - val no_extra = ctx - .annotations - .collectFirst { case _: jsonNoExtraFields => () } - .isDefined || !config.allowExtraFields + val no_extra = ctx.annotations.collectFirst { + case _: jsonNoExtraFields => () + }.isDefined || !config.allowExtraFields if (ctx.params.isEmpty) { new CaseObjectDecoder(ctx, no_extra) } else { new CollectionJsonDecoder[A] { private val (names, aliases): (Array[String], Array[(String, Int)]) = { - val names = Array.ofDim[String](ctx.params.size) + val names = new Array[String](ctx.params.size) val aliasesBuilder = Array.newBuilder[(String, Int)] ctx.params.foreach { var idx = 0 @@ -409,12 +408,11 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv IArray.genericWrapArray(ctx.subtypes.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap: Map[String, Int] = names.zipWithIndex.toMap - def isEnumeration = - (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]])) || ( - !ctx.isEnum && ctx.subtypes.forall(_.isObject) - ) + val isEnumeration = config.enumValuesAsStrings && + (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]]) || + !ctx.isEnum && ctx.subtypes.forall(_.isObject)) - def discrim = + val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (isEnumeration && discrim.isEmpty) { @@ -633,88 +631,124 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv } } - def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { - val isEnumeration = - (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder)) || ( - !ctx.isEnum && ctx.subtypes.forall(_.isObject) - ) + def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { + val isEnumeration = config.enumValuesAsStrings && + (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder) || + !ctx.isEnum && ctx.subtypes.forall(_.isObject)) val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) + val names: Array[String] = IArray.genericWrapArray(ctx.subtypes.map { p => + p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeInfo.short)) + }).toArray val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (isEnumeration && discrim.isEmpty) { new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - JsonEncoder.string.unsafeEncode(name, indent, out) + private val subtypes = ctx.subtypes + + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var i = 0 + while (i < subtypes.length) { + if (subtypes(i).cast.isDefinedAt(a)) { + JsonEncoder.string.unsafeEncode(names(i), indent, out) + return + } + i += 1 + } } - override final def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - new Right(new Json.Str(name)) + override final def toJsonAST(a: A): Either[String, Json] = { + var i = 0 + while (i < subtypes.length) { + if (subtypes(i).cast.isDefinedAt(a)) { + return new Right(new Json.Str(names(i))) + } + i += 1 + } + throw new IllegalArgumentException // shodn't be reached } } } else if (discrim.isEmpty) { new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - JsonEncoder.string.unsafeEncode(name, indent_, out) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) - JsonEncoder.pad(indent, out) - out.write('}') + private val subtypes = ctx.subtypes + + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var i = 0 + while (i < subtypes.length) { + val sub = subtypes(i) + if (sub.cast.isDefinedAt(a)) { + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + JsonEncoder.string.unsafeEncode(names(i), indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) + JsonEncoder.pad(indent, out) + out.write('}') + return + } + i += 1 + } } - override def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).map { inner => - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - new Json.Obj(Chunk(name -> inner)) + override def toJsonAST(a: A): Either[String, Json] = { + var i = 0 + while (i < subtypes.length) { + val sub = subtypes(i) + if (sub.cast.isDefinedAt(a)) { + return sub.typeclass.toJsonAST(sub.cast(a)).map { inner => + new Json.Obj(Chunk(names(i) -> inner)) + } + } + i += 1 } + throw new IllegalArgumentException // shodn't be reached } } } else { new JsonEncoder[A] { - private val hintField = discrim.get + private val subtypes = ctx.subtypes + private val hintFieldName = discrim.get - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - JsonEncoder.string.unsafeEncode(hintField, indent_, out) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - JsonEncoder.string.unsafeEncode(name, indent_, out) - // whitespace is always off by 2 spaces at the end, probably not worth fixing - val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) - sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var i = 0 + while (i < subtypes.length) { + val sub = subtypes(i) + if (sub.cast.isDefinedAt(a)) { + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + JsonEncoder.string.unsafeEncode(hintFieldName, indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + JsonEncoder.string.unsafeEncode(names(i), indent_, out) + // whitespace is always off by 2 spaces at the end, probably not worth fixing + val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) + sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) + return + } + i += 1 + } } - override final def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).flatMap { - case o: Json.Obj => - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - new Right(Json.Obj((hintField -> new Json.Str(name)) +: o.fields)) // hint field is always first - case _ => - new Left("expected object") + override final def toJsonAST(a: A): Either[String, Json] = { + var i = 0 + while (i < subtypes.length) { + val sub = subtypes(i) + if (sub.cast.isDefinedAt(a)) { + return sub.typeclass.toJsonAST(sub.cast(a)).flatMap { + case o: Json.Obj => + val hintField = hintFieldName -> new Json.Str(names(i)) + new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first + case _ => + new Left("expected object") + } + } + i += 1 } + throw new IllegalArgumentException // shodn't be reached } } } diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala index 155d8675..d4ca8d93 100644 --- a/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala @@ -7,11 +7,10 @@ import scala.collection.immutable object CodecVersionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("CodecSpec")( + suite("CodecVersionSpecific")( test("ArraySeq") { val jsonStr = """["5XL","2XL","XL"]""" val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) } ) diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala index 70fb3414..c9af9f4b 100644 --- a/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala @@ -9,12 +9,11 @@ import scala.collection.immutable object DecoderVersionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("Decoder")( + suite("DecoderVersionSpecific")( suite("fromJson")( test("ArraySeq") { val jsonStr = """["5XL","2XL","XL"]""" val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) } ), @@ -22,7 +21,6 @@ object DecoderVersionSpecificSpec extends ZIOSpecDefault { test("ArraySeq") { val json = Json.Arr(Json.Str("5XL"), Json.Str("2XL"), Json.Str("XL")) val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(json.as[Seq[String]])(isRight(equalTo(expected))) } ) diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala index b8c4ac44..aaabfb13 100644 --- a/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala @@ -9,7 +9,7 @@ import scala.collection.immutable object EncoderVesionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("Encoder")( + suite("EncoderVesionSpecific")( suite("toJson")( test("collections") { assert(immutable.ArraySeq[Int]().toJson)(equalTo("[]")) && @@ -22,7 +22,6 @@ object EncoderVesionSpecificSpec extends ZIOSpecDefault { test("collections") { val arrEmpty = Json.Arr() val arr123 = Json.Arr(Json.Num(1), Json.Num(2), Json.Num(3)) - assert(immutable.ArraySeq[Int]().toJsonAST)(isRight(equalTo(arrEmpty))) && assert(immutable.ArraySeq(1, 2, 3).toJsonAST)(isRight(equalTo(arr123))) } diff --git a/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala index 155d8675..d0e7a109 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala @@ -7,12 +7,37 @@ import scala.collection.immutable object CodecVersionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("CodecSpec")( + suite("CodecVersionSpecific")( test("ArraySeq") { val jsonStr = """["5XL","2XL","XL"]""" val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) + }, + test("Derives for a product type") { + assertZIO(typeCheck { + """ + case class Foo(bar: String) derives JsonCodec + + Foo("bar").toJson.fromJson[Foo] + """ + })(isRight(anything)) + }, + test("Derives for a sum type") { + assertZIO(typeCheck { + """ + enum Foo derives JsonCodec: + case Bar + case Baz(baz: String) + case Qux(foo: Foo) + + (Foo.Qux(Foo.Bar): Foo).toJson.fromJson[Foo] + """ + })(isRight(anything)) + }, + test("Derives and encodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonCodec + + assertTrue(Foo("A", Some("A")).toJson.fromJson[Foo] == Right(Foo("A", Some("A")))) } ) } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala index 70fb3414..4fa2639a 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala @@ -9,20 +9,125 @@ import scala.collection.immutable object DecoderVersionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("Decoder")( + suite("DecoderVersionSpecific")( suite("fromJson")( test("ArraySeq") { val jsonStr = """["5XL","2XL","XL"]""" val expected = immutable.ArraySeq("5XL", "2XL", "XL") assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) + }, + test("Derives for a product type") { + case class Foo(bar: String) derives JsonDecoder + + assertTrue("{\"bar\": \"hello\"}".fromJson[Foo] == Right(Foo("hello"))) + }, + test("Derives for a sum enum Enumeration type") { + @jsonHintNames(SnakeCase) + enum Foo derives JsonDecoder: + case Bar + case Baz + case Qux + + assertTrue("\"qux\"".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("\"bar\"".fromJson[Foo] == Right(Foo.Bar)) + }, + test("Derives for a sum enum Enumeration type with enumValuesAsStrings = false") { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + enum Foo derives JsonDecoder: + case Bar + case Baz + case Qux + + assertTrue("{\"Qux\":{}}".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("{\"Bar\":{}}".fromJson[Foo] == Right(Foo.Bar)) + }, + test("Derives for a sum sealed trait Enumeration type") { + sealed trait Foo derives JsonDecoder + object Foo: + @jsonHint("Barrr") + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + assertTrue("\"Qux\"".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("\"Barrr\"".fromJson[Foo] == Right(Foo.Bar)) + }, + test("Derives for a sum sealed trait Enumeration type with enumValuesAsStrings = false") { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + sealed trait Foo derives JsonDecoder + object Foo: + @jsonHint("Barrr") + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + assertTrue("{\"Qux\":{}}".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("{\"Barrr\":{}}".fromJson[Foo] == Right(Foo.Bar)) + }, + test("Derives for a sum sealed trait Enumeration type with discriminator") { + @jsonDiscriminator("$type") + sealed trait Foo derives JsonDecoder + object Foo: + @jsonHint("Barrr") + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + assertTrue("""{"$type":"Qux"}""".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("""{"$type":"Barrr"}""".fromJson[Foo] == Right(Foo.Bar)) + }, + test("skip JSON encoded in a string value") { + @jsonDiscriminator("type") + sealed trait Example derives JsonDecoder { + type Content + def content: Content + } + object Example { + @jsonHint("JSON") + final case class JsonInput(content: String) extends Example { + override type Content = String + } + } + + val json = + """ + |{ + | "content": "\"{\\n \\\"name\\\": \\\"John\\\",\\\"location\\\":\\\"Sydney\\\",\\n \\\"email\\\": \\\"jdoe@test.com\\\"\\n}\"", + | "type": "JSON" + |} + |""".stripMargin.trim + assertTrue(json.fromJson[Example].isRight) + }, + test("Derives for a recursive sum ADT type") { + enum Foo derives JsonDecoder: + case Bar + case Baz(baz: String) + case Qux(foo: Foo) + + assertTrue("{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] == Right(Foo.Qux(Foo.Bar))) + }, + test("Derives and decodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonDecoder + + assertTrue("""{"aOrB": "A", "optA": "A"}""".fromJson[Foo] == Right(Foo("A", Some("A")))) && + assertTrue("""{"aOrB": "C"}""".fromJson[Foo] == Left(".aOrB(expected one of: A, B)")) + }, + test("Derives and decodes for a custom map key string-based union type") { + case class Foo(aOrB: Map["A" | "B", Int]) derives JsonDecoder + + assertTrue("""{"aOrB": {"A": 1, "B": 2}}""".fromJson[Foo] == Right(Foo(Map("A" -> 1, "B" -> 2)))) && + assertTrue("""{"aOrB": {"C": 1}}""".fromJson[Foo] == Left(".aOrB.C(expected one of: A, B)")) } ), suite("fromJsonAST")( test("ArraySeq") { val json = Json.Arr(Json.Str("5XL"), Json.Str("2XL"), Json.Str("XL")) val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(json.as[Seq[String]])(isRight(equalTo(expected))) } ) diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala deleted file mode 100644 index aafb70e4..00000000 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala +++ /dev/null @@ -1,36 +0,0 @@ -package zio.json - -import zio._ -import zio.test.Assertion._ -import zio.test._ - -object DerivedCodecSpec extends ZIOSpecDefault { - val spec = suite("DerivedCodecSpec")( - test("Derives for a product type") { - assertZIO(typeCheck { - """ - case class Foo(bar: String) derives JsonCodec - - Foo("bar").toJson.fromJson[Foo] - """ - })(isRight(anything)) - }, - test("Derives for a sum type") { - assertZIO(typeCheck { - """ - enum Foo derives JsonCodec: - case Bar - case Baz(baz: String) - case Qux(foo: Foo) - - (Foo.Qux(Foo.Bar): Foo).toJson.fromJson[Foo] - """ - })(isRight(anything)) - }, - test("Derives and encodes for a union of string-based literals") { - case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonCodec - - assertTrue(Foo("A", Some("A")).toJson.fromJson[Foo] == Right(Foo("A", Some("A")))) - } - ) -} diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala deleted file mode 100644 index 0007bf61..00000000 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ /dev/null @@ -1,91 +0,0 @@ -package zio.json - -import zio._ -import zio.test.Assertion._ -import zio.test._ - -object DerivedDecoderSpec extends ZIOSpecDefault { - - val spec = suite("DerivedDecoderSpec")( - test("Derives for a product type") { - case class Foo(bar: String) derives JsonDecoder - - assertTrue("{\"bar\": \"hello\"}".fromJson[Foo] == Right(Foo("hello"))) - }, - test("Derives for a sum enum Enumeration type") { - @jsonHintNames(SnakeCase) - enum Foo derives JsonDecoder: - case Bar - case Baz - case Qux - - assertTrue("\"qux\"".fromJson[Foo] == Right(Foo.Qux)) - assertTrue("\"bar\"".fromJson[Foo] == Right(Foo.Bar)) - }, - test("Derives for a sum sealed trait Enumeration type") { - sealed trait Foo derives JsonDecoder - object Foo: - @jsonHint("Barrr") - case object Bar extends Foo - case object Baz extends Foo - case object Qux extends Foo - - assertTrue("\"Qux\"".fromJson[Foo] == Right(Foo.Qux)) - assertTrue("\"Barrr\"".fromJson[Foo] == Right(Foo.Bar)) - }, - test("Derives for a sum sealed trait Enumeration type with discriminator") { - @jsonDiscriminator("$type") - sealed trait Foo derives JsonDecoder - object Foo: - @jsonHint("Barrr") - case object Bar extends Foo - case object Baz extends Foo - case object Qux extends Foo - - assertTrue("""{"$type":"Qux"}""".fromJson[Foo] == Right(Foo.Qux)) - assertTrue("""{"$type":"Barrr"}""".fromJson[Foo] == Right(Foo.Bar)) - }, - test("skip JSON encoded in a string value") { - @jsonDiscriminator("type") - sealed trait Example derives JsonDecoder { - type Content - def content: Content - } - object Example { - @jsonHint("JSON") - final case class JsonInput(content: String) extends Example { - override type Content = String - } - } - - val json = - """ - |{ - | "content": "\"{\\n \\\"name\\\": \\\"John\\\",\\\"location\\\":\\\"Sydney\\\",\\n \\\"email\\\": \\\"jdoe@test.com\\\"\\n}\"", - | "type": "JSON" - |} - |""".stripMargin.trim - assertTrue(json.fromJson[Example].isRight) - }, - test("Derives for a recursive sum ADT type") { - enum Foo derives JsonDecoder: - case Bar - case Baz(baz: String) - case Qux(foo: Foo) - - assertTrue("{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] == Right(Foo.Qux(Foo.Bar))) - }, - test("Derives and decodes for a union of string-based literals") { - case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonDecoder - - assertTrue("""{"aOrB": "A", "optA": "A"}""".fromJson[Foo] == Right(Foo("A", Some("A")))) && - assertTrue("""{"aOrB": "C"}""".fromJson[Foo] == Left(".aOrB(expected one of: A, B)")) - }, - test("Derives and decodes for a custom map key string-based union type") { - case class Foo(aOrB: Map["A" | "B", Int]) derives JsonDecoder - - assertTrue("""{"aOrB": {"A": 1, "B": 2}}""".fromJson[Foo] == Right(Foo(Map("A" -> 1, "B" -> 2)))) && - assertTrue("""{"aOrB": {"C": 1}}""".fromJson[Foo] == Left(".aOrB.C(expected one of: A, B)")) - } - ) -} diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala deleted file mode 100644 index 05430a2c..00000000 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala +++ /dev/null @@ -1,69 +0,0 @@ -package zio.json - -import zio._ -import zio.test.Assertion._ -import zio.test._ - -object DerivedEncoderSpec extends ZIOSpecDefault { - val spec = suite("DerivedEncoderSpec")( - test("Derives for a product type") { - case class Foo(bar: String) derives JsonEncoder - - val json = Foo("bar").toJson - - assertTrue(json == """{"bar":"bar"}""") - }, - test("Derives for a sum enum Enumeration type") { - enum Foo derives JsonEncoder: - case Bar - case Baz - case Qux - - val json = (Foo.Qux: Foo).toJson - - assertTrue(json == """"Qux"""") - }, - test("Derives for a sum enum Enumeration type with discriminator") { - @jsonDiscriminator("$type") - enum Foo derives JsonEncoder: - case Bar - case Baz - case Qux - - val json = (Foo.Qux: Foo).toJson - - assertTrue(json == """{"$type":"Qux"}""") - }, - test("Derives for a sum sealed trait Enumeration type") { - sealed trait Foo derives JsonEncoder - object Foo: - case object Bar extends Foo - case object Baz extends Foo - case object Qux extends Foo - - val json = (Foo.Qux: Foo).toJson - - assertTrue(json == """"Qux"""") - }, - test("Derives for a sum ADT type") { - enum Foo derives JsonEncoder: - case Bar - case Baz(baz: String) - case Qux(foo: Foo) - - val json = (Foo.Qux(Foo.Bar): Foo).toJson - - assertTrue(json == """{"Qux":{"foo":{"Bar":{}}}}""") - }, - test("Derives and encodes for a union of string-based literals") { - case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonEncoder - - assertTrue(Foo("A", Some("A")).toJson == """{"aOrB":"A","optA":"A"}""") - }, - test("Derives and encodes for a custom map key string-based union type") { - case class Foo(aOrB: Map["A" | "B", Int]) derives JsonEncoder - - assertTrue(Foo(Map("A" -> 1, "B" -> 2)).toJson == """{"aOrB":{"A":1,"B":2}}""") - } - ) -} diff --git a/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala index b8c4ac44..2cad2a16 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala @@ -9,13 +9,79 @@ import scala.collection.immutable object EncoderVesionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("Encoder")( + suite("EncoderVesionSpecific")( suite("toJson")( test("collections") { assert(immutable.ArraySeq[Int]().toJson)(equalTo("[]")) && assert(immutable.ArraySeq(1, 2, 3).toJson)(equalTo("[1,2,3]")) && assert(immutable.ArraySeq[String]().toJsonPretty)(equalTo("[]")) && assert(immutable.ArraySeq("foo", "bar").toJsonPretty)(equalTo("[\n \"foo\",\n \"bar\"\n]")) + }, + test("Derives for a product type") { + case class Foo(bar: String) derives JsonEncoder + + val json = Foo("bar").toJson + assertTrue(json == """{"bar":"bar"}""") + }, + test("Derives for a sum enum Enumeration type") { + enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux + + val json = (Foo.Qux: Foo).toJson + assertTrue(json == """"Qux"""") + }, + test("Derives for a sum enum Enumeration type with enumValuesAsStrings = false") { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux + + val json = (Foo.Qux: Foo).toJson + assertTrue(json == """{"Qux":{}}""") + }, + test("Derives for a sum enum Enumeration type with discriminator") { + @jsonDiscriminator("$type") + enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux + + val json = (Foo.Qux: Foo).toJson + assertTrue(json == """{"$type":"Qux"}""") + }, + test("Derives for a sum sealed trait Enumeration type") { + sealed trait Foo derives JsonEncoder + object Foo: + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + val json = (Foo.Qux: Foo).toJson + assertTrue(json == """"Qux"""") + }, + test("Derives for a sum ADT type") { + enum Foo derives JsonEncoder: + case Bar + case Baz(baz: String) + case Qux(foo: Foo) + + val json = (Foo.Qux(Foo.Bar): Foo).toJson + assertTrue(json == """{"Qux":{"foo":{"Bar":{}}}}""") + }, + test("Derives and encodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonEncoder + + assertTrue(Foo("A", Some("A")).toJson == """{"aOrB":"A","optA":"A"}""") + }, + test("Derives and encodes for a custom map key string-based union type") { + case class Foo(aOrB: Map["A" | "B", Int]) derives JsonEncoder + + assertTrue(Foo(Map("A" -> 1, "B" -> 2)).toJson == """{"aOrB":{"A":1,"B":2}}""") } ), suite("toJsonAST")( diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index d22a683a..56dcd4aa 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -350,6 +350,18 @@ object DecoderSpec extends ZIOSpecDefault { assert("""{"Child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && assert("""{"type":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) }, + test("sum encoding with enumValuesAsStrings = true") { + import examplesumobjects1._ + + assert(""""Child1"""".fromJson[Parent])(isRight(equalTo(Child1))) && + assert(""""Child2"""".fromJson[Parent])(isRight(equalTo(Child2))) + }, + test("sum encoding with enumValuesAsStrings = false") { + import examplesumobjects2._ + + assert("""{"Child1":{}}""".fromJson[Parent])(isRight(equalTo(Child1))) && + assert("""{"Child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2))) + }, test("sum encoding with hint names") { import examplesumhintnames._ @@ -896,6 +908,38 @@ object DecoderSpec extends ZIOSpecDefault { } + object examplesumobjects1 { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = true) + + sealed abstract class Parent + + object Parent { + implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] + } + + case object Child1 extends Parent + + case object Child2 extends Parent + + } + + object examplesumobjects2 { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + sealed abstract class Parent + + object Parent { + implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] + } + + case object Child1 extends Parent + + case object Child2 extends Parent + + } + object examplesumhintnames { @jsonHintNames(CamelCase) diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index c210f703..39985aa7 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -376,6 +376,18 @@ object EncoderSpec extends ZIOSpecDefault { assert((Child1(): Parent).toJsonPretty)(equalTo("{\n \"Child1\" : {}\n}")) && assert((Child2(): Parent).toJsonPretty)(equalTo("{\n \"Cain\" : {}\n}")) }, + test("sum encoding with enumValuesAsStrings = true") { + import examplesumobjects1._ + + assert((Child1: Parent).toJson)(equalTo(""""Child1"""")) && + assert((Child2: Parent).toJson)(equalTo(""""Cain"""")) + }, + test("sum encoding with enumValuesAsStrings = false") { + import examplesumobjects2._ + + assert((Child1: Parent).toJson)(equalTo("""{"Child1":{}}""")) && + assert((Child2: Parent).toJson)(equalTo("""{"Cain":{}}""")) + }, test("sum alternative encoding") { import examplealtsum._ @@ -588,6 +600,40 @@ object EncoderSpec extends ZIOSpecDefault { } + object examplesumobjects1 { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = true) + + sealed abstract class Parent + + object Parent { + implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] + } + + case object Child1 extends Parent + + @jsonHint("Cain") + case object Child2 extends Parent + + } + + object examplesumobjects2 { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + sealed abstract class Parent + + object Parent { + implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] + } + + case object Child1 extends Parent + + @jsonHint("Cain") + case object Child2 extends Parent + + } + object examplesumhintnames { @jsonHintNames(CamelCase)