zio / zio-schema

Compositional, type-safe schema definitions, which enable auto-derivation of codecs and migrations.
https://zio.dev/zio-schema
Apache License 2.0
140 stars 160 forks source link

JsonCodec.jsonDecoder does not seem to work with schemas derived from case classes with more than 22 fields #691

Closed stanislav-chetvertkov closed 2 months ago

stanislav-chetvertkov commented 3 months ago

JsonCodec.jsonDecoder does not seem to work with schemas derived from case classes with more than 22 fields, in the following example

import zio.Scope
import zio.json.JsonDecoder
import zio.schema.codec.JsonCodec
import zio.schema.{DeriveSchema, Schema}
import zio.test._

import scala.annotation.nowarn

@nowarn("msg=missing interpolator")
object F22Spec extends ZIOSpecDefault {

  case class Example(
    f1: Option[String],
    f2: Option[String] = None,
    f3: Option[String] = None,
    f4: Option[String] = None,
    f5: Option[String] = None,
    f6: Option[String] = None,
    f7: Option[String] = None,
    f8: Option[String] = None,
    f9: Option[String] = None,
    f10: Option[String] = None,
    f11: Option[String] = None,
    f12: Option[String] = None,
    f13: Option[String] = None,
    f14: Option[String] = None,
    f15: Option[String] = None,
    f16: Option[String] = None,
    f17: Option[String] = None,
    f18: Option[String] = None,
    f19: Option[String] = None,
    f20: Option[String] = None,
    f21: Option[String] = None,
    f22: Option[String] = None,
//    f23: Option[String] = None
                        )

  implicit val exampleSchema: Schema[Example] = DeriveSchema.gen[Example]

  override def spec: Spec[TestEnvironment with Scope, Any] = {
    suite("F22")(
      test("should work with more than 22 fields") {
        import zio.json.yaml.DecoderYamlOps

        implicit val decoder: JsonDecoder[Example] = JsonCodec.jsonDecoder(exampleSchema)

        val string = """f1: test"""

        string.fromYaml[Example] match {
          case Left(error) =>
            TestResult(TestArrow.make(_ => TestTrace.fail(ErrorMessage.text(error))))
          case Right(value) =>
            print(value)
            assertTrue(true)
        }
      }
    )
  }
}

It returns Example(Some(test),None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None)

but when I uncomment f23: Option[String] = None I get (Field f2 is missing) error It could be due to some inconsistencies handling CaseClass22 and GenericRecord (for 23 or more fields) schemas

stanislav-chetvertkov commented 3 months ago

the same behaviour also appears when using

import zio.json.DecoderOps
...
string.fromJson[Example]

I was able to make it work by changing recordDecoder method in zio.schema.codec.JsonCodec to look like this

    private def recordDecoder[Z](structure: Seq[Schema.Field[Z, _]]): ZJsonDecoder[ListMap[String, Any]] = {
      (trace: List[JsonError], in: RetractReader) => {
        val builder: ChunkBuilder[(String, Any)] = zio.ChunkBuilder.make[(String, Any)](structure.size)
        Lexer.char(trace, in, '{')
        if (Lexer.firstField(trace, in)) {
          while ( {
            val field = Lexer.string(trace, in).toString
            structure.find(_.name == field) match {
              case Some(Schema.Field(label, schema, _, _, _, _)) =>
                val trace_ = JsonError.ObjectAccess(label) :: trace
                Lexer.char(trace_, in, ':')
                val value = schemaDecoder(schema).unsafeDecode(trace_, in)
                builder += ((JsonFieldDecoder.string.unsafeDecodeField(trace_, label), value))
              case None =>
                Lexer.char(trace, in, ':')
                Lexer.skipValue(trace, in)

            }
            (Lexer.nextField(trace, in))
          }) {
            ()
          }
        }
        val tuples = builder.result()
        val collectedFields: Set[String] = tuples.map { case (fieldName, _) => fieldName }.toSet
        val resultBuilder = ListMap.newBuilder[String, Any]

        // add fields with default values if they are not present in the JSON
        structure.foreach { field =>
          if (!collectedFields.contains(field.name)) {
            val value = field.name -> field.defaultValue.get
            resultBuilder += value
          }
        }
        (resultBuilder ++= tuples).result()
      }
    }

so that it adds fields with their default values from the schema if they were not present in the input JSON

I'm going create a pr containing the change soon