Skip to content

Commit 6c77e62

Browse files
committed
Fix ClassCastException for hierarchical enum JSON encoding (#668)
- Add constructEnumCase helper to handle both CaseClass0 and Enum case schemas - Update caseMap and jsonFieldDecoder to use type-safe case construction - Add regression tests for hierarchical sealed trait enums Fixes #668
1 parent d8e692f commit 6c77e62

File tree

2 files changed

+96
-3
lines changed

2 files changed

+96
-3
lines changed

zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -636,11 +636,27 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
636636
}
637637
}
638638

639+
private def constructEnumCase[Z, A](case_ : Schema.Case[Z, A]): Z =
640+
case_.schema match {
641+
case cc: Schema.CaseClass0[A] =>
642+
case_.construct(cc.defaultConstruct())
643+
case e: Schema.Enum[A] =>
644+
e.defaultValue match {
645+
case Right(v) => case_.construct(v)
646+
case Left(err) =>
647+
throw new RuntimeException(s"Cannot construct enum case ${case_.id}: $err")
648+
}
649+
case other =>
650+
throw new RuntimeException(
651+
s"Unsupported case schema type for ${case_.id}: ${other.getClass.getSimpleName}"
652+
)
653+
}
654+
639655
private def caseMap[Z](schema: Schema.Enum[Z], cfg: Configuration): Map[Z, String] =
640656
schema.nonTransientCases
641657
.map(
642658
case_ =>
643-
case_.schema.asInstanceOf[Schema.CaseClass0[Z]].defaultConstruct() ->
659+
constructEnumCase(case_) ->
644660
format(case_.caseName, cfg)
645661
)
646662
.toMap
@@ -936,7 +952,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
936952
new JsonFieldDecoder[Z] {
937953
private[this] val stringMatrix = new StringMatrix(caseNameAliases.keys.toArray)
938954
private[this] val cases = caseNameAliases.values.map { case_ =>
939-
case_.schema.asInstanceOf[Schema.CaseClass0[Any]].defaultConstruct()
955+
constructEnumCase(case_)
940956
}.toArray.asInstanceOf[Array[Z]]
941957

942958
override def unsafeDecodeField(trace: List[JsonError], in: String): Z = {
@@ -951,7 +967,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
951967

952968
caseNameAliases.foreach {
953969
case (name, case_) =>
954-
cases.put(name, case_.schema.asInstanceOf[Schema.CaseClass0[Z]].defaultConstruct())
970+
cases.put(name, constructEnumCase(case_))
955971
}
956972

957973
override def unsafeDecodeField(trace: List[JsonError], in: String): Z = {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package zio.schema.codec
2+
3+
import zio.schema._
4+
import zio.test._
5+
6+
/**
7+
* Regression test for issue #668:
8+
* JSON codec built from auto-derived schema fails for enumeration with intermediate type
9+
*
10+
* The bug occurred when encoding hierarchical sealed trait enums where intermediate
11+
* types exist (e.g., Animal > Mammal > Bison). The JsonCodec was incorrectly assuming
12+
* all enum cases have CaseClass0 schemas, but intermediate sealed traits have Enum schemas.
13+
*/
14+
object JsonCodecSpec668 extends ZIOSpecDefault {
15+
16+
// Reproducer from issue #668
17+
sealed trait Animal
18+
object Animal {
19+
sealed trait Mammal extends Animal
20+
case object Bison extends Mammal
21+
22+
implicit val schema: Schema[Animal] = DeriveSchema.gen[Animal]
23+
}
24+
25+
// Additional test case with deeper hierarchy
26+
sealed trait Vehicle
27+
object Vehicle {
28+
sealed trait LandVehicle extends Vehicle
29+
sealed trait Car extends LandVehicle
30+
case object Sedan extends Car
31+
case object Truck extends LandVehicle
32+
33+
implicit val schema: Schema[Vehicle] = DeriveSchema.gen[Vehicle]
34+
}
35+
36+
def spec = suite("JsonCodec Issue #668 - Hierarchical Enums")(
37+
test("should encode case object from intermediate sealed trait") {
38+
val codec = JsonCodec.jsonCodec(Animal.schema)
39+
val value: Animal = Animal.Bison
40+
41+
for {
42+
encoded <- codec.encoder.encode(value)
43+
decoded <- codec.decoder.decode(encoded)
44+
} yield assertTrue(decoded == value)
45+
},
46+
47+
test("should encode deeply nested enum hierarchy") {
48+
val codec = JsonCodec.jsonCodec(Vehicle.schema)
49+
val sedan: Vehicle = Vehicle.Sedan
50+
val truck: Vehicle = Vehicle.Truck
51+
52+
for {
53+
encodedSedan <- codec.encoder.encode(sedan)
54+
decodedSedan <- codec.decoder.decode(encodedSedan)
55+
encodedTruck <- codec.encoder.encode(truck)
56+
decodedTruck <- codec.decoder.decode(encodedTruck)
57+
} yield assertTrue(
58+
decodedSedan == sedan,
59+
decodedTruck == truck
60+
)
61+
},
62+
63+
test("should handle round-trip encoding/decoding") {
64+
val codec = JsonCodec.jsonCodec(Animal.schema)
65+
val value: Animal = Animal.Bison
66+
67+
for {
68+
json <- codec.encoder.encode(value)
69+
result <- codec.decoder.decode(json)
70+
reencoded <- codec.encoder.encode(result)
71+
} yield assertTrue(
72+
result == value,
73+
reencoded == json
74+
)
75+
}
76+
)
77+
}

0 commit comments

Comments
 (0)