Summary
kindlings-jsoniter-derivation's KindlingsJsonValueCodec encodes a parameterless singleton member of a sealed hierarchy (a case object, or a no-arg Scala 3 enum case) as {"Name":{}}, but the generated decoder cannot read that back — it throws:
com.github.plokhotnyuk.jsoniter_scala.core.JsonReaderException: expected '}' after wrapped enum value, offset: 0x00000007
So encode → decode is not a round-trip for parameterless singletons. Members that carry fields (case class Circle(r: Int)) round-trip fine, so this is specific to the empty-{} wrapped payload the encoder emits for parameterless cases.
Reproduction
//> using scala 3.8.4
//> using dep com.kubuszok::kindlings-jsoniter-derivation:0.3.1
import com.github.plokhotnyuk.jsoniter_scala.core.*
import hearth.kindlings.jsoniterderivation.{ JsoniterConfig, KindlingsJsonValueCodec }
given JsoniterConfig = JsoniterConfig()
sealed trait Color derives KindlingsJsonValueCodec
object Color {
case object Red extends Color
case object Green extends Color
}
@main def run(): Unit =
val value: Color = Color.Red
val json = writeToString(value)
println(s"encoded: $json") // {"Red":{}}
val decoded = readFromString[Color](json) // <-- throws
println(s"decoded: $decoded")
assert(decoded == value)
println("round-trip OK")
Actual behaviour
encoded: {"Red":{}}
Exception in thread "main" com.github.plokhotnyuk.jsoniter_scala.core.JsonReaderException: expected '}' after wrapped enum value, offset: 0x00000007, buf:
+----------+-------------------------------------------------+------------------+
| | 0 1 2 3 4 5 6 7 8 9 a b c d e f | 0123456789abcdef |
+----------+-------------------------------------------------+------------------+
| 00000000 | 7b 22 52 65 64 22 3a 7b 7d 7d | {"Red":{}} |
+----------+-------------------------------------------------+------------------+
Offset 0x07 is the opening { of the inner {} — after reading the "Red" discriminator key the reader does not consume the empty-object value the encoder wrote.
Expected behaviour
readFromString[Color](writeToString(v)) == v for every case, i.e. {"Red":{}} decodes to Color.Red (or encoder and decoder agree on some other symmetric shape for parameterless singletons).
Scope
-
Reproduces on kindlings 0.3.1 and 0.3.0 (Scala 3.8.4, JVM).
-
Affects both sealed trait + case object and parameterless Scala 3 enum cases:
enum Direction derives KindlingsJsonValueCodec:
case North, South
// writeToString(Direction.North) == {"North":{}} -> same decode failure
-
Does not affect subtypes that carry fields — final case class Circle(r: Int) extends Shape encodes as {"Circle":{"r":3}} and round-trips correctly.
-
Default JsoniterConfig(). Setting discriminatorFieldName changes the encoded shape to {"type":"Red"} but the decoder still fails (expected end of input), so it is not a config workaround.
Environment
- Scala 3.8.4, JDK (temurin) 17 / JVM
com.kubuszok::kindlings-jsoniter-derivation:0.3.1 (and 0.3.0)
- jsoniter-scala-core (transitive)
Found while migrating branchtalk-backend; a sealed SessionType { case object UserSession; case object OAuth } in an HTTP response body failed to deserialize in integration tests.
Summary
kindlings-jsoniter-derivation'sKindlingsJsonValueCodecencodes a parameterless singleton member of a sealed hierarchy (acase object, or a no-arg Scala 3enumcase) as{"Name":{}}, but the generated decoder cannot read that back — it throws:So encode → decode is not a round-trip for parameterless singletons. Members that carry fields (
case class Circle(r: Int)) round-trip fine, so this is specific to the empty-{}wrapped payload the encoder emits for parameterless cases.Reproduction
Actual behaviour
Offset
0x07is the opening{of the inner{}— after reading the"Red"discriminator key the reader does not consume the empty-object value the encoder wrote.Expected behaviour
readFromString[Color](writeToString(v)) == vfor every case, i.e.{"Red":{}}decodes toColor.Red(or encoder and decoder agree on some other symmetric shape for parameterless singletons).Scope
Reproduces on kindlings 0.3.1 and 0.3.0 (Scala 3.8.4, JVM).
Affects both
sealed trait+case objectand parameterless Scala 3enumcases:Does not affect subtypes that carry fields —
final case class Circle(r: Int) extends Shapeencodes as{"Circle":{"r":3}}and round-trips correctly.Default
JsoniterConfig(). SettingdiscriminatorFieldNamechanges the encoded shape to{"type":"Red"}but the decoder still fails (expected end of input), so it is not a config workaround.Environment
com.kubuszok::kindlings-jsoniter-derivation:0.3.1(and0.3.0)Found while migrating branchtalk-backend; a sealed
SessionType { case object UserSession; case object OAuth }in an HTTP response body failed to deserialize in integration tests.