From 67936bba55bf7bb0535746c514d96d7ac9787f78 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Wed, 6 Sep 2023 06:14:48 +0200 Subject: [PATCH] Ways to ignore empty collections while encoding json --- .../schema/example/example1/Example1.scala | 22 ++- .../schema/example/example2/Example2.scala | 12 +- .../schema/example/example3/Example3.scala | 24 ++- .../scala/zio/schema/codec/JsonCodec.scala | 178 ++++++++++-------- .../zio/schema/codec/JsonCodecSpec.scala | 119 ++++++++++-- 5 files changed, 246 insertions(+), 109 deletions(-) diff --git a/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example1/Example1.scala b/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example1/Example1.scala index 7a9b2188a..b1736f561 100644 --- a/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example1/Example1.scala +++ b/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example1/Example1.scala @@ -145,9 +145,11 @@ object JsonSample extends zio.ZIOAppDefault { override def run: ZIO[Environment with ZIOAppArgs, Any, Any] = for { - _ <- ZIO.unit - person = Person("Michelle", 32) - personToJsonTransducer = JsonCodec.schemaBasedBinaryCodec[Person](schemaPerson).streamEncoder + _ <- ZIO.unit + person = Person("Michelle", 32) + personToJsonTransducer = JsonCodec + .schemaBasedBinaryCodec[Person](schemaPerson, JsonCodec.Config.default) + .streamEncoder _ <- ZStream(person) .via(personToJsonTransducer) .via(ZPipeline.utf8Decode) @@ -192,8 +194,8 @@ object CombiningExample extends ZIOAppDefault { _ <- ZIO.debug("combining roundtrip") person = Person("Michelle", 32) - personToJson = JsonCodec.schemaBasedBinaryCodec[Person](schemaPerson).streamEncoder - jsonToPerson = JsonCodec.schemaBasedBinaryCodec[Person](schemaPerson).streamDecoder + personToJson = JsonCodec.schemaBasedBinaryCodec[Person](schemaPerson, JsonCodec.Config.default).streamEncoder + jsonToPerson = JsonCodec.schemaBasedBinaryCodec[Person](schemaPerson, JsonCodec.Config.default).streamDecoder personToProto = ProtobufCodec.protobufCodec[Person](schemaPerson).streamEncoder protoToPerson = ProtobufCodec.protobufCodec[Person](schemaPerson).streamDecoder @@ -226,10 +228,16 @@ object DictionaryExample extends ZIOAppDefault { person = Person("Mike", 32) dictionary = Map("m" -> person) dictionaryToJson = JsonCodec - .schemaBasedBinaryCodec[scala.collection.immutable.Map[String, Person]](schemaPersonDictionaryFromMacro) + .schemaBasedBinaryCodec[scala.collection.immutable.Map[String, Person]]( + schemaPersonDictionaryFromMacro, + JsonCodec.Config.default + ) .streamEncoder jsonToDictionary = JsonCodec - .schemaBasedBinaryCodec[scala.collection.immutable.Map[String, Person]](schemaPersonDictionaryFromMacro) + .schemaBasedBinaryCodec[scala.collection.immutable.Map[String, Person]]( + schemaPersonDictionaryFromMacro, + JsonCodec.Config.default + ) .streamDecoder newPersonDictionary <- ZStream(dictionary) .via(dictionaryToJson) diff --git a/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example2/Example2.scala b/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example2/Example2.scala index 3bc56004e..833ee869d 100644 --- a/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example2/Example2.scala +++ b/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example2/Example2.scala @@ -156,9 +156,11 @@ object JsonSample extends zio.ZIOAppDefault { override def run: ZIO[Environment with ZIOAppArgs, Any, Any] = for { - _ <- ZIO.unit - person = Person("Michelle", 32) - personToJsonPipeline = JsonCodec.schemaBasedBinaryCodec[Person](Person.schema).streamEncoder + _ <- ZIO.unit + person = Person("Michelle", 32) + personToJsonPipeline = JsonCodec + .schemaBasedBinaryCodec[Person](Person.schema, JsonCodec.Config.default) + .streamEncoder _ <- ZStream(person) .via(personToJsonPipeline) .via(ZPipeline.utf8Decode) @@ -201,8 +203,8 @@ object CombiningExample extends zio.ZIOAppDefault { _ <- ZIO.debug("combining roundtrip") person = Person("Michelle", 32) - personToJson = JsonCodec.schemaBasedBinaryCodec[Person](Person.schema).streamEncoder - jsonToPerson = JsonCodec.schemaBasedBinaryCodec[Person](Person.schema).streamDecoder + personToJson = JsonCodec.schemaBasedBinaryCodec[Person](Person.schema, JsonCodec.Config.default).streamEncoder + jsonToPerson = JsonCodec.schemaBasedBinaryCodec[Person](Person.schema, JsonCodec.Config.default).streamDecoder personToProto = ProtobufCodec.protobufCodec[Person](Person.schema).streamEncoder protoToPerson = ProtobufCodec.protobufCodec[Person](Person.schema).streamDecoder diff --git a/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example3/Example3.scala b/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example3/Example3.scala index 127fb89ce..2c33a3064 100644 --- a/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example3/Example3.scala +++ b/zio-schema-examples/shared/src/main/scala/dev/zio/schema/example/example3/Example3.scala @@ -73,16 +73,24 @@ object Example3 extends ZIOAppDefault { _ <- ZIO.debug("input JSON : " + json) // get objects from JSON - personDTO <- ZIO.fromEither(JsonCodec.schemaBasedBinaryCodec[PersonDTO](PersonDTO.schema).decode(chunks)) - person <- ZIO.fromEither(JsonCodec.schemaBasedBinaryCodec[Person](personTransformation).decode(chunks)) - _ <- ZIO.debug("PersonDTO : " + personDTO) - _ <- ZIO.debug("Person : " + person) + personDTO <- ZIO.fromEither( + JsonCodec.schemaBasedBinaryCodec[PersonDTO](PersonDTO.schema, JsonCodec.Config.default).decode(chunks) + ) + person <- ZIO.fromEither( + JsonCodec.schemaBasedBinaryCodec[Person](personTransformation, JsonCodec.Config.default).decode(chunks) + ) + _ <- ZIO.debug("PersonDTO : " + personDTO) + _ <- ZIO.debug("Person : " + person) // get JSON from Objects - personJson = new String(JsonCodec.schemaBasedBinaryCodec[Person](Person.schema).encode(person).toArray) - personDTOJson = new String(JsonCodec.schemaBasedBinaryCodec[Person](personTransformation).encode(person).toArray) - _ <- ZIO.debug("Person JSON: " + personJson) - _ <- ZIO.debug("PersonDTO JSON: " + personDTOJson) + personJson = new String( + JsonCodec.schemaBasedBinaryCodec[Person](Person.schema, JsonCodec.Config.default).encode(person).toArray + ) + personDTOJson = new String( + JsonCodec.schemaBasedBinaryCodec[Person](personTransformation, JsonCodec.Config.default).encode(person).toArray + ) + _ <- ZIO.debug("Person JSON: " + personJson) + _ <- ZIO.debug("PersonDTO JSON: " + personDTOJson) } yield () } diff --git a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala index 6ee664ad9..5cc3ee676 100644 --- a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala +++ b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala @@ -23,6 +23,13 @@ import zio.stream.ZPipeline import zio.{ Cause, Chunk, ChunkBuilder, NonEmptyChunk, ZIO } object JsonCodec { + + final case class Config(ignoreEmptyCollections: Boolean) + + object Config { + val default: Config = Config(ignoreEmptyCollections = false) + } + type DiscriminatorTuple = Chunk[(discriminatorName, String)] implicit def zioJsonBinaryCodec[A](implicit jsonCodec: ZJsonCodec[A]): BinaryCodec[A] = @@ -58,7 +65,7 @@ object JsonCodec { ) } - implicit def schemaBasedBinaryCodec[A](implicit schema: Schema[A]): BinaryCodec[A] = + implicit def schemaBasedBinaryCodec[A](implicit schema: Schema[A], cfg: Config): BinaryCodec[A] = new BinaryCodec[A] { override def decode(whole: Chunk[Byte]): Either[DecodeError, A] = JsonDecoder.decode( @@ -79,7 +86,7 @@ object JsonCodec { } override def encode(value: A): Chunk[Byte] = - JsonEncoder.encode(schema, value) + JsonEncoder.encode(schema, value, cfg) override def streamEncoder: ZPipeline[Any, Nothing, A, Byte] = ZPipeline.mapChunks( @@ -88,7 +95,10 @@ object JsonCodec { } def jsonEncoder[A](schema: Schema[A]): ZJsonEncoder[A] = - JsonEncoder.schemaEncoder(schema) + JsonEncoder.schemaEncoder(schema, JsonCodec.Config.default) + + def jsonEncoder[A](cfg: JsonCodec.Config)(schema: Schema[A]): ZJsonEncoder[A] = + JsonEncoder.schemaEncoder(schema, cfg) def jsonDecoder[A](schema: Schema[A]): ZJsonDecoder[A] = JsonDecoder.schemaDecoder(schema) @@ -96,6 +106,9 @@ object JsonCodec { def jsonCodec[A](schema: Schema[A]): ZJsonCodec[A] = ZJsonCodec(jsonEncoder(schema), jsonDecoder(schema)) + def jsonCodec[A](cfg: JsonCodec.Config)(schema: Schema[A]): ZJsonCodec[A] = + ZJsonCodec(jsonEncoder(cfg)(schema), jsonDecoder(schema)) + object Codecs { protected[codec] val unitEncoder: ZJsonEncoder[Unit] = (_: Unit, _: Option[Int], out: Write) => out.write("{}") @@ -155,8 +168,8 @@ object JsonCodec { private[codec] val CHARSET = StandardCharsets.UTF_8 - final def encode[A](schema: Schema[A], value: A): Chunk[Byte] = - charSequenceToByteChunk(schemaEncoder(schema).encodeJson(value, None)) + final def encode[A](schema: Schema[A], value: A, cfg: Config): Chunk[Byte] = + charSequenceToByteChunk(schemaEncoder(schema, cfg).encodeJson(value, None)) private[codec] def charSequenceToByteChunk(chars: CharSequence): Chunk[Byte] = { val bytes = CHARSET.newEncoder().encode(CharBuffer.wrap(chars)) @@ -164,102 +177,102 @@ object JsonCodec { } //scalafmt: { maxColumn = 400, optIn.configStyleArguments = false } - private[codec] def schemaEncoder[A](schema: Schema[A], discriminatorTuple: DiscriminatorTuple = Chunk.empty): ZJsonEncoder[A] = + private[codec] def schemaEncoder[A](schema: Schema[A], cfg: Config, discriminatorTuple: DiscriminatorTuple = Chunk.empty): ZJsonEncoder[A] = schema match { case Schema.Primitive(standardType, _) => primitiveCodec(standardType).encoder - case Schema.Sequence(schema, _, g, _, _) => ZJsonEncoder.chunk(schemaEncoder(schema, discriminatorTuple)).contramap(g) - case Schema.Map(ks, vs, _) => mapEncoder(ks, vs, discriminatorTuple) + case Schema.Sequence(schema, _, g, _, _) => ZJsonEncoder.chunk(schemaEncoder(schema, cfg, discriminatorTuple)).contramap(g) + case Schema.Map(ks, vs, _) => mapEncoder(ks, vs, discriminatorTuple, cfg) case Schema.Set(s, _) => - ZJsonEncoder.chunk(schemaEncoder(s, discriminatorTuple)).contramap(m => Chunk.fromIterable(m)) - case Schema.Transform(c, _, g, a, _) => transformEncoder(a.foldLeft(c)((s, a) => s.annotate(a)), g) - case Schema.Tuple2(l, r, _) => ZJsonEncoder.tuple2(schemaEncoder(l, discriminatorTuple), schemaEncoder(r, discriminatorTuple)) - case Schema.Optional(schema, _) => ZJsonEncoder.option(schemaEncoder(schema, discriminatorTuple)) + ZJsonEncoder.chunk(schemaEncoder(s, cfg, discriminatorTuple)).contramap(m => Chunk.fromIterable(m)) + case Schema.Transform(c, _, g, a, _) => transformEncoder(a.foldLeft(c)((s, a) => s.annotate(a)), g, cfg) + case Schema.Tuple2(l, r, _) => ZJsonEncoder.tuple2(schemaEncoder(l, cfg, discriminatorTuple), schemaEncoder(r, cfg, discriminatorTuple)) + case Schema.Optional(schema, _) => ZJsonEncoder.option(schemaEncoder(schema, cfg, discriminatorTuple)) case Schema.Fail(_, _) => unitEncoder.contramap(_ => ()) - case Schema.GenericRecord(_, structure, _) => recordEncoder(structure.toChunk) - case Schema.Either(left, right, _) => ZJsonEncoder.either(schemaEncoder(left, discriminatorTuple), schemaEncoder(right, discriminatorTuple)) - case l @ Schema.Lazy(_) => schemaEncoder(l.schema, discriminatorTuple) - case Schema.CaseClass0(_, _, _) => caseClassEncoder(schema, discriminatorTuple) - case Schema.CaseClass1(_, f, _, _) => caseClassEncoder(schema, discriminatorTuple, f) - case Schema.CaseClass2(_, f1, f2, _, _) => caseClassEncoder(schema, discriminatorTuple, f1, f2) - case Schema.CaseClass3(_, f1, f2, f3, _, _) => caseClassEncoder(schema, discriminatorTuple, f1, f2, f3) + case Schema.GenericRecord(_, structure, _) => recordEncoder(structure.toChunk, cfg) + case Schema.Either(left, right, _) => ZJsonEncoder.either(schemaEncoder(left, cfg, discriminatorTuple), schemaEncoder(right, cfg, discriminatorTuple)) + case l @ Schema.Lazy(_) => schemaEncoder(l.schema, cfg, discriminatorTuple) + case Schema.CaseClass0(_, _, _) => caseClassEncoder(schema, discriminatorTuple, cfg) + case Schema.CaseClass1(_, f, _, _) => caseClassEncoder(schema, discriminatorTuple, cfg, f) + case Schema.CaseClass2(_, f1, f2, _, _) => caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2) + case Schema.CaseClass3(_, f1, f2, f3, _, _) => caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3) case Schema.CaseClass4(_, f1, f2, f3, f4, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4) case Schema.CaseClass5(_, f1, f2, f3, f4, f5, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5) case Schema.CaseClass6(_, f1, f2, f3, f4, f5, f6, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6) case Schema.CaseClass7(_, f1, f2, f3, f4, f5, f6, f7, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7) case Schema.CaseClass8(_, f1, f2, f3, f4, f5, f6, f7, f8, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8) case Schema .CaseClass9(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9) case Schema.CaseClass10(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10) case Schema.CaseClass11(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11) case Schema.CaseClass12(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12) case Schema.CaseClass13(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13) case Schema.CaseClass14(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14) case Schema.CaseClass15(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15) case Schema.CaseClass16(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16) case Schema.CaseClass17(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17) case Schema.CaseClass18(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18) case Schema.CaseClass19(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, _, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19) case Schema.CaseClass20(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, _) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20) case Schema.CaseClass21(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, tail) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, tail._1) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, tail._1) case Schema.CaseClass22(_, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, tail) => - caseClassEncoder(schema, discriminatorTuple, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, tail._1, tail._2) - case e @ Schema.Enum1(_, c, _) => enumEncoder(e, c) - case e @ Schema.Enum2(_, c1, c2, _) => enumEncoder(e, c1, c2) - case e @ Schema.Enum3(_, c1, c2, c3, _) => enumEncoder(e, c1, c2, c3) - case e @ Schema.Enum4(_, c1, c2, c3, c4, _) => enumEncoder(e, c1, c2, c3, c4) - case e @ Schema.Enum5(_, c1, c2, c3, c4, c5, _) => enumEncoder(e, c1, c2, c3, c4, c5) - case e @ Schema.Enum6(_, c1, c2, c3, c4, c5, c6, _) => enumEncoder(e, c1, c2, c3, c4, c5, c6) - case e @ Schema.Enum7(_, c1, c2, c3, c4, c5, c6, c7, _) => enumEncoder(e, c1, c2, c3, c4, c5, c6, c7) - case e @ Schema.Enum8(_, c1, c2, c3, c4, c5, c6, c7, c8, _) => enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8) - case e @ Schema.Enum9(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, _) => enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9) + caseClassEncoder(schema, discriminatorTuple, cfg, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, tail._1, tail._2) + case e @ Schema.Enum1(_, c, _) => enumEncoder(e, cfg, c) + case e @ Schema.Enum2(_, c1, c2, _) => enumEncoder(e, cfg, c1, c2) + case e @ Schema.Enum3(_, c1, c2, c3, _) => enumEncoder(e, cfg, c1, c2, c3) + case e @ Schema.Enum4(_, c1, c2, c3, c4, _) => enumEncoder(e, cfg, c1, c2, c3, c4) + case e @ Schema.Enum5(_, c1, c2, c3, c4, c5, _) => enumEncoder(e, cfg, c1, c2, c3, c4, c5) + case e @ Schema.Enum6(_, c1, c2, c3, c4, c5, c6, _) => enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6) + case e @ Schema.Enum7(_, c1, c2, c3, c4, c5, c6, c7, _) => enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7) + case e @ Schema.Enum8(_, c1, c2, c3, c4, c5, c6, c7, c8, _) => enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8) + case e @ Schema.Enum9(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, _) => enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9) case e @ Schema.Enum10(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) case e @ Schema.Enum11(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) case e @ Schema.Enum12(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) case e @ Schema.Enum13(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13) case e @ Schema.Enum14(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14) case e @ Schema.Enum15(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15) case e @ Schema.Enum16(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16) case e @ Schema.Enum17(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17) case e @ Schema.Enum18(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18) case e @ Schema.Enum19(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19) case e @ Schema .Enum20(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20) case e @ Schema .Enum21(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21) case e @ Schema.Enum22(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21, c22, _) => - enumEncoder(e, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21, c22) - case e @ Schema.EnumN(_, cs, _) => enumEncoder(e, cs.toSeq: _*) - case d @ Schema.Dynamic(_) => dynamicEncoder(d) + enumEncoder(e, cfg, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21, c22) + case e @ Schema.EnumN(_, cs, _) => enumEncoder(e, cfg, cs.toSeq: _*) + case d @ Schema.Dynamic(_) => dynamicEncoder(d, cfg) case _ => if (schema eq null) throw new Exception(s"A captured schema is null, most likely due to wrong field initialization order") @@ -280,20 +293,21 @@ object JsonCodec { private[codec] def mapEncoder[K, V]( ks: Schema[K], vs: Schema[V], - discriminatorTuple: DiscriminatorTuple + discriminatorTuple: DiscriminatorTuple, + cfg: Config ): ZJsonEncoder[Map[K, V]] = { - val valueEncoder = JsonEncoder.schemaEncoder(vs) + val valueEncoder = JsonEncoder.schemaEncoder(vs, cfg) jsonFieldEncoder(ks) match { case Some(jsonFieldEncoder) => ZJsonEncoder.keyValueIterable(jsonFieldEncoder, valueEncoder).contramap(a => Chunk.fromIterable(a).toMap) case None => ZJsonEncoder - .chunk(schemaEncoder(ks, discriminatorTuple).zip(schemaEncoder(vs, discriminatorTuple))) + .chunk(schemaEncoder(ks, cfg, discriminatorTuple).zip(schemaEncoder(vs, cfg, discriminatorTuple))) .contramap(m => Chunk.fromIterable(m)) } } - private def dynamicEncoder(schema: Schema.Dynamic): ZJsonEncoder[DynamicValue] = { + private def dynamicEncoder(schema: Schema.Dynamic, cfg: Config): ZJsonEncoder[DynamicValue] = { val directMapping = schema.annotations.exists { case directDynamicMapping() => true case _ => false @@ -357,13 +371,13 @@ object JsonCodec { } } } else { - schemaEncoder(DynamicValue.schema) + schemaEncoder(DynamicValue.schema, cfg) } } - private def transformEncoder[A, B](schema: Schema[A], g: B => Either[String, A]): ZJsonEncoder[B] = + private def transformEncoder[A, B](schema: Schema[A], g: B => Either[String, A], cfg: Config): ZJsonEncoder[B] = new ZJsonEncoder[B] { - private lazy val innerEncoder = schemaEncoder(schema) + private lazy val innerEncoder = schemaEncoder(schema, cfg) override def unsafeEncode(b: B, indent: Option[Int], out: Write): Unit = g(b) match { @@ -378,7 +392,7 @@ object JsonCodec { } } - private def enumEncoder[Z](parentSchema: Schema.Enum[Z], cases: Schema.Case[Z, _]*): ZJsonEncoder[Z] = + private def enumEncoder[Z](parentSchema: Schema.Enum[Z], cfg: Config, cases: Schema.Case[Z, _]*): ZJsonEncoder[Z] = // if all cases are CaseClass0, encode as a String if (parentSchema.annotations.exists(_.isInstanceOf[simpleEnum])) { val caseMap: Map[Z, String] = cases @@ -428,6 +442,7 @@ object JsonCodec { schemaEncoder( case_.schema.asInstanceOf[Schema[Any]], + cfg, discriminatorTuple = if (noDiscriminators) Chunk.empty else discriminatorChunk ).unsafeEncode(case_.deconstruct(value), indent, out) @@ -438,7 +453,7 @@ object JsonCodec { } } - private def recordEncoder[Z](structure: Seq[Schema.Field[Z, _]]): ZJsonEncoder[ListMap[String, _]] = { + private def recordEncoder[Z](structure: Seq[Schema.Field[Z, _]], cfg: Config): ZJsonEncoder[ListMap[String, _]] = { (value: ListMap[String, _], indent: Option[Int], out: Write) => { if (structure.isEmpty) { @@ -450,7 +465,7 @@ object JsonCodec { var first = true structure.foreach { case Schema.Field(k, a, _, _, _, _) => - val enc = schemaEncoder(a.asInstanceOf[Schema[Any]]) + val enc = schemaEncoder(a.asInstanceOf[Schema[Any]], cfg) if (first) first = false else { @@ -751,7 +766,19 @@ object JsonCodec { private[codec] object ProductEncoder { import ZJsonEncoder.{ bump, pad } - private[codec] def caseClassEncoder[Z](parentSchema: Schema[_], discriminatorTuple: DiscriminatorTuple, fields: Schema.Field[Z, _]*): ZJsonEncoder[Z] = { (a: Z, indent: Option[Int], out: Write) => + private[codec] def isEmptyOptionalValue(schema: Schema.Field[_, _], value: Any, cfg: Config) = { + val ignoreEmptyCollections = + cfg.ignoreEmptyCollections || schema.annotations.contains(optionalField()) + + val isEmptyCollection = value match { + case _: Iterable[_] => value.asInstanceOf[Iterable[_]].isEmpty + case _ => false + } + + ignoreEmptyCollections && isEmptyCollection + } + + private[codec] def caseClassEncoder[Z](parentSchema: Schema[_], discriminatorTuple: DiscriminatorTuple, cfg: Config, fields: Schema.Field[Z, _]*): ZJsonEncoder[Z] = { (a: Z, indent: Option[Int], out: Write) => { out.write('{') val indent_ = bump(indent) @@ -773,11 +800,12 @@ object JsonCodec { case s: Schema.Field[Z, _] => val enc = try { - JsonEncoder.schemaEncoder(s.schema) + JsonEncoder.schemaEncoder(s.schema, cfg) } catch { case e: Throwable => throw new RuntimeException(s"Failed to encode field '${s.name}' in $parentSchema'", e) } - if (!enc.isNothing(s.get(a))) { + val value = s.get(a) + if (!enc.isNothing(value) && !isEmptyOptionalValue(s, value, cfg)) { if (first) first = false else { diff --git a/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala b/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala index b3b66901f..868befead 100644 --- a/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala +++ b/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala @@ -60,6 +60,63 @@ object JsonCodecSpec extends ZIOSpecDefault { ) } ), + suite("optional field annotation")( + test("list empty") { + assertEncodesJson( + Schema[WithOptField], + WithOptField(Nil, Map("foo" -> 1)), + """{"map":{"foo":1}}""" + ) + }, + test("map empty") { + assertEncodesJson( + Schema[WithOptField], + WithOptField(List("foo"), Map.empty), + """{"list":["foo"]}""" + ) + }, + test("all empty") { + assertEncodesJson( + Schema[WithOptField], + WithOptField(Nil, Map.empty), + """{}""" + ) + } + ), + suite("empty collections config")( + test("list empty") { + assertEncodesJson( + Schema[ListAndMap], + ListAndMap(Nil, Map("foo" -> 1)), + """{"map":{"foo":1}}""", + JsonCodec.Config(ignoreEmptyCollections = true) + ) + }, + test("map empty") { + assertEncodesJson( + Schema[ListAndMap], + ListAndMap(List("foo"), Map.empty), + """{"list":["foo"]}""", + JsonCodec.Config(ignoreEmptyCollections = true) + ) + }, + test("all empty") { + assertEncodesJson( + Schema[ListAndMap], + ListAndMap(Nil, Map.empty), + """{}""", + JsonCodec.Config(ignoreEmptyCollections = true) + ) + }, + test("all empty, but don't ignore empty collections") { + assertEncodesJson( + Schema[ListAndMap], + ListAndMap(Nil, Map.empty), + """{"list":[],"map":{}}""", + JsonCodec.Config(ignoreEmptyCollections = false) + ) + } + ), suite("tuple")( test("of primitives") { assertEncodesJson( @@ -1114,8 +1171,8 @@ object JsonCodecSpec extends ZIOSpecDefault { val dyn = DynamicValue.fromSchemaAndValue(schema, value) ZStream .succeed(dyn) - .via(JsonCodec.schemaBasedBinaryCodec(Schema.dynamicValue).streamEncoder) - .via(JsonCodec.schemaBasedBinaryCodec(Schema.dynamicValue).streamDecoder) + .via(JsonCodec.schemaBasedBinaryCodec(Schema.dynamicValue, JsonCodec.Config.default).streamEncoder) + .via(JsonCodec.schemaBasedBinaryCodec(Schema.dynamicValue, JsonCodec.Config.default).streamDecoder) .map(_.toTypedValue(schema)) .runHead .map { result => @@ -1149,10 +1206,16 @@ object JsonCodecSpec extends ZIOSpecDefault { ) ) - private def assertEncodes[A](schema: Schema[A], value: A, chunk: Chunk[Byte], print: Boolean = false) = { + private def assertEncodes[A]( + schema: Schema[A], + value: A, + chunk: Chunk[Byte], + cfg: JsonCodec.Config = JsonCodec.Config.default, + print: Boolean = false + ) = { val stream = ZStream .succeed(value) - .via(JsonCodec.schemaBasedBinaryCodec(schema).streamEncoder) + .via(JsonCodec.schemaBasedBinaryCodec(schema, cfg).streamEncoder) .runCollect .tap { chunk => printLine(s"${new String(chunk.toArray)}").when(print).ignore @@ -1160,10 +1223,15 @@ object JsonCodecSpec extends ZIOSpecDefault { assertZIO(stream)(equalTo(chunk)) } - private def assertEncodesJson[A](schema: Schema[A], value: A, json: String) = { + private def assertEncodesJson[A]( + schema: Schema[A], + value: A, + json: String, + cfg: JsonCodec.Config = JsonCodec.Config.default + ) = { val stream = ZStream .succeed(value) - .via(JsonCodec.schemaBasedBinaryCodec[A](schema).streamEncoder) + .via(JsonCodec.schemaBasedBinaryCodec[A](schema, cfg).streamEncoder) .runCollect .map(chunk => new String(chunk.toArray)) assertZIO(stream)(equalTo(json)) @@ -1172,22 +1240,32 @@ object JsonCodecSpec extends ZIOSpecDefault { private def assertEncodesJson[A](schema: Schema[A], value: A)(implicit enc: JsonEncoder[A]) = { val stream = ZStream .succeed(value) - .via(JsonCodec.schemaBasedBinaryCodec[A](schema).streamEncoder) + .via(JsonCodec.schemaBasedBinaryCodec[A](schema, JsonCodec.Config.default).streamEncoder) .runCollect assertZIO(stream)(equalTo(jsonEncoded(value))) } - private def assertDecodesToError[A](schema: Schema[A], json: CharSequence, errors: List[JsonError]) = { + private def assertDecodesToError[A]( + schema: Schema[A], + json: CharSequence, + errors: List[JsonError], + cfg: JsonCodec.Config = JsonCodec.Config.default + ) = { val stream = ZStream .fromChunk(charSequenceToByteChunk(json)) - .via(JsonCodec.schemaBasedBinaryCodec[A](schema).streamDecoder) + .via(JsonCodec.schemaBasedBinaryCodec[A](schema, cfg).streamDecoder) .catchAll(ZStream.succeed[DecodeError](_)) .runHead assertZIO(stream)(isSome(equalTo(ReadError(Cause.empty, JsonError.render(errors))))) } - private def assertDecodes[A](schema: Schema[A], value: A, chunk: Chunk[Byte]) = { - val result = ZStream.fromChunk(chunk).via(JsonCodec.schemaBasedBinaryCodec[A](schema).streamDecoder).runCollect + private def assertDecodes[A]( + schema: Schema[A], + value: A, + chunk: Chunk[Byte], + cfg: JsonCodec.Config = JsonCodec.Config.default + ) = { + val result = ZStream.fromChunk(chunk).via(JsonCodec.schemaBasedBinaryCodec[A](schema, cfg).streamDecoder).runCollect assertZIO(result)(equalTo(Chunk(value))) } @@ -1199,18 +1277,19 @@ object JsonCodecSpec extends ZIOSpecDefault { decodingSchema: Schema[A2], value: A1, compare: (A1, A2) => Boolean, - print: Boolean + print: Boolean, + cfg: JsonCodec.Config = JsonCodec.Config.default ) = ZStream .succeed(value) .tap(value => printLine(s"Input Value: $value").when(print).ignore) - .via(JsonCodec.schemaBasedBinaryCodec[A1](encodingSchema).streamEncoder) + .via(JsonCodec.schemaBasedBinaryCodec[A1](encodingSchema, cfg).streamEncoder) .runCollect .tap(encoded => printLine(s"Encoded: ${new String(encoded.toArray)}").when(print).ignore) .flatMap { encoded => ZStream .fromChunk(encoded) - .via(JsonCodec.schemaBasedBinaryCodec[A2](decodingSchema).streamDecoder) + .via(JsonCodec.schemaBasedBinaryCodec[A2](decodingSchema, cfg).streamDecoder) .runCollect .tapError { err => printLineError(s"Decoding failed for input ${new String(encoded.toArray)}\nError Message: $err") @@ -1486,4 +1565,16 @@ object JsonCodecSpec extends ZIOSpecDefault { object WithOptionFields { implicit lazy val schema: Schema[WithOptionFields] = DeriveSchema.gen[WithOptionFields] } + + final case class WithOptField(@optionalField list: List[String], @optionalField map: Map[String, Int]) + + object WithOptField { + implicit lazy val schema: Schema[WithOptField] = DeriveSchema.gen[WithOptField] + } + + final case class ListAndMap(list: List[String], map: Map[String, Int]) + + object ListAndMap { + implicit lazy val schema: Schema[ListAndMap] = DeriveSchema.gen[ListAndMap] + } }