From 0e6970289450eeb53a6e9227977ac60f5c84da9e Mon Sep 17 00:00:00 2001 From: Juliano Alves Date: Wed, 18 Sep 2024 05:44:07 +0100 Subject: [PATCH] generate empty collections for missing fields (#741) --- .../scala/zio/schema/codec/JsonCodec.scala | 17 ++++-- .../zio/schema/codec/JsonCodecSpec.scala | 55 +++++++++++++++++++ .../src/main/scala/zio/schema/Schema.scala | 9 +++ 3 files changed, 77 insertions(+), 4 deletions(-) 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 ee9c14a3d..19aa3fc78 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 @@ -1523,12 +1523,21 @@ object JsonCodec { var i = 0 while (i < len) { if (buffer(i) == null) { - - if ((fields(i).optional || fields(i).transient) && fields(i).defaultValue.isDefined) + if ((fields(i).optional || fields(i).transient) && fields(i).defaultValue.isDefined) { buffer(i) = fields(i).defaultValue.get - else - buffer(i) = schemaDecoder(schemas(i)).unsafeDecodeMissing(spans(i) :: trace) + } else { + val schema = fields(i).schema match { + case l @ Schema.Lazy(_) => l.schema + case _ => schemas(i) + } + schema match { + case collection: Schema.Collection[_, _] => + buffer(i) = collection.empty + case _ => + buffer(i) = schemaDecoder(schema).unsafeDecodeMissing(spans(i) :: trace) + } + } } i += 1 } diff --git a/zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala b/zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala index 0d3e37175..b6dd57558 100644 --- a/zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala +++ b/zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala @@ -1051,6 +1051,43 @@ object JsonCodecSpec extends ZIOSpecDefault { ) } ), + suite("Missing collection fields")( + test("map") { + assertDecodes( + Schema[ListAndMapAndOption], + ListAndMapAndOption(Nil, Map.empty, None), + charSequenceToByteChunk("""{"list":[]}""") + ) + }, + test("list") { + assertDecodes( + Schema[ListAndMapAndOption], + ListAndMapAndOption(Nil, Map.empty, None), + charSequenceToByteChunk("""{"map":{}}""") + ) + }, + test("set") { + assertDecodes( + Schema[SetWrapper], + SetWrapper(Set.empty), + charSequenceToByteChunk("""{}""") + ) + }, + test("vector") { + assertDecodes( + Schema[VectorWrapper], + VectorWrapper(Vector.empty), + charSequenceToByteChunk("""{}""") + ) + }, + test("chunck") { + assertDecodes( + Schema[ChunckWrapper], + ChunckWrapper(Chunk.empty), + charSequenceToByteChunk("""{}""") + ) + } + ), suite("zio.json.ast.Json decoding")( test("Json.Obj") { assertDecodes( @@ -2172,6 +2209,24 @@ object JsonCodecSpec extends ZIOSpecDefault { implicit lazy val schema: Schema[ListAndMapAndOption] = DeriveSchema.gen[ListAndMapAndOption] } + final case class SetWrapper(set: Set[String]) + + object SetWrapper { + implicit lazy val schema: Schema[SetWrapper] = DeriveSchema.gen[SetWrapper] + } + + final case class VectorWrapper(sequence: Vector[String]) + + object VectorWrapper { + implicit lazy val schema: Schema[VectorWrapper] = DeriveSchema.gen[VectorWrapper] + } + + final case class ChunckWrapper(chunk: Chunk[String]) + + object ChunckWrapper { + implicit lazy val schema: Schema[ChunckWrapper] = DeriveSchema.gen[ChunckWrapper] + } + final case class KeyWrapper(key: String) object KeyWrapper { diff --git a/zio-schema/shared/src/main/scala/zio/schema/Schema.scala b/zio-schema/shared/src/main/scala/zio/schema/Schema.scala index 59a0f617f..56763d16e 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/Schema.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/Schema.scala @@ -512,6 +512,7 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { sealed trait Collection[Col, Elem] extends Schema[Col] { def fromChunk: Chunk[Elem] => Col def toChunk: Col => Chunk[Elem] + def empty: Col } final case class Sequence[Col, Elem, I]( @@ -534,6 +535,7 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { override def toString: String = s"Sequence($elementSchema, $identity)" + override def empty: Col = fromChunk(Chunk.empty[Elem]) } final case class Transform[A, B, I]( @@ -840,6 +842,7 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { override val toChunk: scala.collection.immutable.Map[K, V] => Chunk[(K, V)] = map => Chunk.fromIterable(map.toList) + override def empty: scala.collection.immutable.Map[K, V] = scala.collection.immutable.Map.empty[K, V] } final case class NonEmptyMap[K, V]( @@ -874,6 +877,9 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { override def makeAccessors(b: AccessorBuilder): b.Traversal[prelude.NonEmptyMap[K, V], (K, V)] = b.makeTraversal(self, keySchema <*> valueSchema) + + override def empty: prelude.NonEmptyMap[K, V] = + throw new IllegalArgumentException("NonEmptyMap cannot be empty") } final case class NonEmptySequence[Col, Elm, I]( @@ -900,6 +906,8 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { override def makeAccessors(b: AccessorBuilder): b.Traversal[Col, Elm] = b.makeTraversal(self, elementSchema) override def toString: String = s"NonEmptySequence($elementSchema, $identity)" + + override def empty: Col = throw new IllegalArgumentException(s"NonEmptySequence $identity cannot be empty") } final case class Set[A](elementSchema: Schema[A], override val annotations: Chunk[Any] = Chunk.empty) @@ -923,6 +931,7 @@ object Schema extends SchemaPlatformSpecific with SchemaEquality { override val toChunk: scala.collection.immutable.Set[A] => Chunk[A] = Chunk.fromIterable(_) + override def empty: scala.collection.immutable.Set[A] = scala.collection.immutable.Set.empty[A] } final case class Dynamic(override val annotations: Chunk[Any] = Chunk.empty) extends Schema[DynamicValue] {