json4s icon indicating copy to clipboard operation
json4s copied to clipboard

CustomSerializer[Enumeration.Value] clobbers EnumNameSerializer()

Open izcet opened this issue 4 years ago • 2 comments

Context

Hi, I have kind of an unusual edge case. I recently had to change an enum class to be backwards-compatible with some legacy code, but expanded and renamed new terms. This works entirely fine and is not the focus, but what surfaced this issue.

Previously, I could (de)serialize just fine with these Formats:

implicit val formats = DefaultFormats + new EnumNameSerializer(DayOfWeek)

But since the change I had to define a CustomSerializer

implicit val formats = DefaultFormats + DayOfWeekSerializer
class DayOfWeekSerializer extends CustomSerializer[DayOfWeek.Value](format => (
  { case Jstring(value) => DayOfWeek.customParse(value) },
  { case v: DayOfWeek.Value => JString(v.toString) }
))
object DayOfWeek extends Enumeration {
  val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday = Value

  /*
   * We used to only store abbreviated weekdays, so now we have to handle parsing them
   * If it's not one of our special cases, we can rely on the default behavior.
   * Note we cannot override withName since it is final. Found that out the hard way.
   */  
  def customParse(value: String): Value = {
    value match {
      case "Mon" => Monday
      case "Tue" => Tuesday
      case "Wed" => Wednesday
      case "Thu" => Thursday
      case "Fri" => Friday
      case _ => values.withName(value)
    }
  }
}

That works just fine... until I add another Enum into the mix. This breaks, full stack trace below.

implicit val formats = DefaultFormats + new EnumNameSerializer(Color) + new DayOfWeekSerializer

Attempted workarounds:

  • Add conditional to the case in the DayOfWeekSerializer { case JString(value) if Try(DayOfWeek.customParse(value)).isSuccess => DayOfWeek.customParse(value) } Does not work, it seems the conditional is ignored completely
  • Swap order of formats implicit val formats = DefaultFormats + new DayOfWeekSerializer + new EnumNameSerializer(Color) swapping the order of the serializers works and the code runs as expected without error. That being said, I'd rather not have to rely on ordering because it's not immediately clear.

Ask

I think the ordering of Formats should not matter, though I will admit my understanding of the depths of json4s is not thorough. From my limited perspective as a user, I would expect ordering to not be relevant. Or at least, even if ordering is relevant, it won't break other formats. I do get that Enums are a difficult edge case since they carry no class data at runtime.

If there's something more/different/better I could be doing with the CustomSerializer implementation please let me know. (I can also cross-post to StackOverflow if you prefer)

Stacktrace

[error]  org.json4s.package$MappingException: No usable value for color
[error]  Can't convert JString(GREEN) to class scala.Enumeration$Value (package.scala:95)
[error] org.json4s.reflect.package$.fail(package.scala:95)
[error] org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$buildCtorArg(Extraction.scala:548)
[error] org.json4s.Extraction$ClassInstanceBuilder$$anonfun$3.applyOrElse(Extraction.scala:572)
[error] org.json4s.Extraction$ClassInstanceBuilder$$anonfun$3.applyOrElse(Extraction.scala:570)
[error] org.json4s.Extraction$ClassInstanceBuilder.instantiate(Extraction.scala:570)
[error] org.json4s.Extraction$ClassInstanceBuilder.result(Extraction.scala:630)
[error] org.json4s.Extraction$.$anonfun$extract$10(Extraction.scala:416)
[error] org.json4s.Extraction$.$anonfun$customOrElse$1(Extraction.scala:637)
[error] org.json4s.Extraction$.customOrElse(Extraction.scala:637)
[error] org.json4s.Extraction$.extract(Extraction.scala:408)
[error] org.json4s.Extraction$.extract(Extraction.scala:40)
[error] org.json4s.ExtractableJsonAstNode.extract(ExtractableJsonAstNode.scala:21)
[error] org.json4s.native.Serialization$.read(Serialization.scala:71)
[error] org.json4s.Serialization.read(Serialization.scala:25)
[error] org.json4s.Serialization.read$(Serialization.scala:25)
[error] org.json4s.native.Serialization$.read(Serialization.scala:32)
[error] /* three lines of application code */
[error] org.json4s.CustomSerializer$$anonfun$deserialize$3.applyOrElse(Formats.scala:485)
[error] org.json4s.CustomSerializer$$anonfun$deserialize$3.applyOrElse(Formats.scala:482)
[error] org.json4s.Extraction$.customOrElse(Extraction.scala:637)
[error] org.json4s.Extraction$.extract(Extraction.scala:408)
[error] org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$buildCtorArg(Extraction.scala:534)
[error] org.json4s.Extraction$ClassInstanceBuilder$$anonfun$3.applyOrElse(Extraction.scala:572)
[error] org.json4s.Extraction$ClassInstanceBuilder$$anonfun$3.applyOrElse(Extraction.scala:570)
[error] org.json4s.Extraction$ClassInstanceBuilder.instantiate(Extraction.scala:570)
[error] org.json4s.Extraction$ClassInstanceBuilder.result(Extraction.scala:630)
[error] org.json4s.Extraction$.$anonfun$extract$10(Extraction.scala:416)
[error] org.json4s.Extraction$.$anonfun$customOrElse$1(Extraction.scala:637)
[error] org.json4s.Extraction$.customOrElse(Extraction.scala:637)
[error] org.json4s.Extraction$.extract(Extraction.scala:408)
[error] org.json4s.Extraction$.extract(Extraction.scala:40)
[error] org.json4s.ExtractableJsonAstNode.extract(ExtractableJsonAstNode.scala:21)
[error] org.json4s.native.Serialization$.read(Serialization.scala:71)
[error] org.json4s.Serialization.read(Serialization.scala:25)
[error] org.json4s.Serialization.read$(Serialization.scala:25)
[error] org.json4s.native.Serialization$.read(Serialization.scala:32)
[error] /* same 3 lines again */
[error] CAUSED BY
[error]  org.json4s.package$MappingException: Can't convert JString(GREEN) to class scala.Enumeration$Value (Formats.scala:485)
[error] org.json4s.CustomSerializer$$anonfun$deserialize$3.applyOrElse(Formats.scala:485)
[error] org.json4s.CustomSerializer$$anonfun$deserialize$3.applyOrElse(Formats.scala:482)
[error] org.json4s.Extraction$.customOrElse(Extraction.scala:637)
[error] org.json4s.Extraction$.extract(Extraction.scala:408)
[error] org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$buildCtorArg(Extraction.scala:534)
[error] org.json4s.Extraction$ClassInstanceBuilder$$anonfun$3.applyOrElse(Extraction.scala:572)
[error] org.json4s.Extraction$ClassInstanceBuilder$$anonfun$3.applyOrElse(Extraction.scala:570)
[error] org.json4s.Extraction$ClassInstanceBuilder.instantiate(Extraction.scala:570)
[error] org.json4s.Extraction$ClassInstanceBuilder.result(Extraction.scala:630)
[error] org.json4s.Extraction$.$anonfun$extract$10(Extraction.scala:416)
[error] org.json4s.Extraction$.$anonfun$customOrElse$1(Extraction.scala:637)
[error] org.json4s.Extraction$.customOrElse(Extraction.scala:637)
[error] org.json4s.Extraction$.extract(Extraction.scala:408)
[error] org.json4s.Extraction$.extract(Extraction.scala:40)
[error] org.json4s.ExtractableJsonAstNode.extract(ExtractableJsonAstNode.scala:21)
[error] org.json4s.native.Serialization$.read(Serialization.scala:71)
[error] org.json4s.Serialization.read(Serialization.scala:25)
[error] org.json4s.Serialization.read$(Serialization.scala:25)
[error] org.json4s.native.Serialization$.read(Serialization.scala:32)
[error] /* same 3 lines, entry point */

json4s version

3.6.+

scala version

2.12.6

jdk version

OpenJdk 1.8.0

izcet avatar Aug 06 '20 23:08 izcet

I realize I left out what I was actually attempting.

case class Foo(color: Color.Value, day: DayOfWeek.Value)

// ...
implicit val formats = <...from above...>

val json: String = """{"color":"GREEN","day":"Thursday"}"""

val foo = read[Foo](json) // breaks here

izcet avatar Aug 06 '20 23:08 izcet

I'm late to the party but I believe the problems you're having are inherent to the Scala 2 enum implementation where ALL enums actually are actually of the same type. This means that there is no reasonable way for json4s to distinguish between them.

If you have the freedom to do so, I would recommend that you either a) switch to a different enum implementation, such as enumeratum (which is great) or b) just use plain Java-enums

tjarvstrand avatar Mar 03 '21 12:03 tjarvstrand