Skip to content

jsoniter: parameterless singletons (case object / no-arg enum cases) encode but fail to decode #196

Description

@MateuszKubuszok

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions