Open michaelitindi opened 1 month ago
The plan to solve the bug involves addressing the field order sensitivity in the JSON decoding logic. The issue arises when the ADT field is not the last field in the case class, causing subsequent fields to be incorrectly decoded. By ensuring that the unsafeDecodeFields
method correctly handles fields in any order and improving the handling of discriminators, we can resolve the bug.
The bug is caused by the unsafeDecodeFields
method's inability to correctly decode fields that follow an ADT field when the ADT field is not the last field in the case class. This is likely due to incorrect buffer initialization or field mapping logic, which fails to correctly populate the buffer with decoded values when the field order is not as expected.
To fix the bug, we need to modify the unsafeDecodeFields
method in zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala
to correctly handle fields in any order. Additionally, we need to add test cases to zio-schema-json/shared/src/test/scala-3/zio/schema/codec/JsonCodecSpec.scala
to verify the fix.
JsonCodec.scala
// zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala
// Modify the unsafeDecodeFields method to handle fields in any order
def unsafeDecodeFields(json: Json, schema: Schema[_]): Either[String, Any] = {
val buffer = Array.fill[Any](schema.fields.size)(null)
val fieldMap = schema.fields.zipWithIndex.toMap
json.asObject.foreach { jsonObj =>
jsonObj.fields.foreach { case (fieldName, fieldValue) =>
fieldMap.get(fieldName).foreach { index =>
buffer(index) = fieldValue
}
}
}
// Ensure all fields are decoded
schema.fields.zip(buffer).foreach { case (field, value) =>
if (value == null) {
return Left(s"Missing field: ${field.name}")
}
}
Right(schema.construct(buffer))
}
JsonCodecSpec.scala
// zio-schema-json/shared/src/test/scala-3/zio/schema/codec/JsonCodecSpec.scala
import zio.schema.annotation.discriminatorName
import zio.*
import zio.json.*
import zio.schema.*
import zio.test.*
import zio.test.Assertion.*
object JsonCodecSpec extends ZIOSpecDefault {
@discriminatorName("type")
sealed trait Content derives Schema
object Content {
given JsonCodec[Content] = zio.schema.codec.JsonCodec.jsonCodec(Schema[Content])
case object AAA extends Content
case object BBB extends Content
case class CCC(a: Int) extends Content
}
case class Wrapper(a: Int, c: Content, b: Boolean) derives Schema
object Wrapper {
import Content.given
given JsonCodec[Wrapper] = zio.schema.codec.JsonCodec.jsonCodec(Schema[Wrapper])
}
val v = Wrapper(a = 1, c = Content.AAA, b = true)
val str = v.toJson
def spec = suite("JsonCodecSpec")(
test("decode Wrapper with ADT field in the middle") {
assert(str.fromJson[Wrapper])(isRight(equalTo(v)))
},
test("decode Wrapper with ADT field at the end") {
case class WrapperAlt(a: Int, b: Boolean, c: Content) derives Schema
object WrapperAlt {
import Content.given
given JsonCodec[WrapperAlt] = zio.schema.codec.JsonCodec.jsonCodec(Schema[WrapperAlt])
}
val vAlt = WrapperAlt(a = 1, b = true, c = Content.AAA)
val strAlt = vAlt.toJson
assert(strAlt.fromJson[WrapperAlt])(isRight(equalTo(vAlt)))
}
)
}
To replicate the bug, follow these steps:
By running the provided test cases, you can observe the decoding failure when the ADT field is in the middle and verify the fix when the ADT field is at the end.
Click here to create a Pull Request with the proposed solution
Files used for this task:
Description JSON decoding bug occurs when attempting to parse parameters following a sealed trait with mixed types (case objects and a case class). The decoder fails to process any parameters that follow the ADT parameter unless the fields in the case class containing the ADT are reordered.
Steps to Reproduce: Define a sealed trait with mixed type implementations (case objects and a case class) and a discriminator. Implement a wrapper case class that includes this ADT as a parameter among other basic type parameters. Use ZIO JSON to derive codecs for both the ADT and wrapper class. Encode an instance of the wrapper class to JSON and decode it back to verify the integrity of all fields. Minimal Code to Reproduce import zio.schema.annotation.discriminatorName import zio. import zio.json. import zio.schema.*
@discriminatorName("type") sealed trait Content derives Schema
object Content { given JsonCodec[Content] = zio.schema.codec.JsonCodec.jsonCodec(Schema[Content])
case object AAA extends Content case object BBB extends Content case class CCC(a: Int) extends Content }
case class Wrapper(a: Int, c: Content, b: Boolean) derives Schema // case class Wrapper(a: Int, b: Boolean, c: Content) derives Schema // decoding success with this order object Wrapper { import Content.given
given JsonCodec[Wrapper] = zio.schema.codec.JsonCodec.jsonCodec(Schema[Wrapper]) }
val v = Wrapper(a = 1, c = Content.AAA, b = true) val str = v.toJson println(s"Decoding from String: ${str.fromJson[Wrapper]}") Expected Behavior: The decoder should reconstruct the Wrapper instance correctly, preserving the order and integrity of fields (a, b, c)
Actual Behavior: When the ADT field c is placed between two other fields, decoding fails for the fields that follow the ADT field. However, rearranging the fields in the Wrapper class (placing the ADT field at the end) resolves the issue.
Environment: ZIO Schema: 1.2.2 ZIO JSON: 0.7.1 Scala: 3.4.2