【问题标题】:Fail-fast json4s serialisation of sealed trait and object enum when missing serializer缺少序列化程序时,密封特征和对象枚举的快速 json4s 序列化
【发布时间】:2019-09-07 05:36:27
【问题描述】:

设置

我正在使用 json4s 3.2.11 和 Scala 2.11。

我有一个使用sealed trait 定义的枚举和一个自定义序列化程序:

import org.json4s.CustomSerializer
import org.json4s.JsonAST.JString
import org.json4s.DefaultFormats
import org.json4s.jackson.Serialization

sealed trait Foo
case object X extends Foo
case object Y extends Foo

object FooSerializer
    extends CustomSerializer[Foo](
      _ =>
        ({
          case JString("x") => X
          case JString("y") => Y
        }, {
          case X => JString("x")
          case Y => JString("y")
        })
    )

这很棒,添加到格式时效果很好:

{
  implicit val formats = DefaultFormats + FooSerializer
  Serialization.write(X) // "x"
}

这太棒了!

问题

如果格式中没有添加序列化器,json4s 会使用反射来创建字段的默认表示,这对于这些没有字段的objects 非常无益。它默默地执行此操作,似乎无法控制它。

{
  implicit val formats = DefaultFormats
  Serialization.write(X) // {}
}

这是一个问题,因为直到很久以后才有迹象表明出了什么问题。如果测试没有碰巧捕捉到这些无效/无用数据,则可能会通过网络发送或写入数据库。而且,这可能会从图书馆公开暴露,这意味着下游用户也必须记住它。

注意。这与read 不同,后者在失败时抛出异常,因为Foo trait 没有任何有用的构造函数:

{
  implicit val formats = DefaultFormats
  Serialization.read[Foo]("\"x\"")
}
org.json4s.package$MappingException: No constructor for type Foo, JString(x)
  at org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$constructor(Extraction.scala:417)
  at org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$instantiate(Extraction.scala:468)
  at org.json4s.Extraction$ClassInstanceBuilder$$anonfun$result$6.apply(Extraction.scala:515)
...

问题

有没有办法禁用这些对象的默认{} 格式,或者将格式“烘焙”到对象本身?

例如,让write 抛出类似read 的异常就可以了,因为它会立即将问题标记给调用者。

【问题讨论】:

    标签: scala serialization reflection json4s


    【解决方案1】:

    有一个旧的open issue 似乎提出了类似的问题,其中一个contributors 建议

    您需要创建自定义反序列化器或序列化器

    这表明没有开箱即用的方法来改变默认行为。

    方法 1:通过 Scalastyle 禁止默认格式

    尝试使用 Scalastyle IllegalImportsChecker 禁止导入 org.json4s.DefaultFormats

     <check level="error" class="org.scalastyle.scalariform.IllegalImportsChecker" enabled="true">
      <parameters>
       <customMessage>Import from illegal package: Please use example.DefaultFormats instead of org.json4s.DefaultFormats</customMessage>
       <parameter name="illegalImports"><![CDATA[org.json4s.DefaultFormats]]></parameter>
      </parameters>
     </check>
    

    并像这样提供自定义DefaultFormats

    package object example {
      val DefaultFormats = Serialization.formats(NoTypeHints) + FooSerializer
    }
    

    这将允许我们像这样序列化 ADT

    import example.DefaultFormats
    implicit val formats = DefaultFormats
    case class Bar(foo: Foo)
    println(Serialization.write(Bar(X)))
    println(Serialization.write(X))
    println(Serialization.write(Y))
    

    应该输出哪个

    {"foo":"x"}
    "x"
    "y"
    

    如果我们尝试导入 org.json4s.DefaultFormats,那么 Scalastyle 应该会引发以下错误:

    Import from illegal package: Please use example.DefaultFormats instead of org.json4s.DefaultFormats
    

    方法2:对非嵌套值进行序列化

    也许我们可以通过在Foo 中定义write 方法将格式“烘焙”到对象中,该方法委托给Serialization.write,就像这样

    sealed trait Foo {
      object FooSerializer extends CustomSerializer[Foo](_ =>
          ({
            case JString("x") => X
            case JString("y") => Y
          }, {
            case X => JString("x")
            case Y => JString("y")
          })
      )
    
      def write: String = 
        Serialization.write(this)(DefaultFormats + FooSerializer)
    }
    case object X extends Foo
    case object Y extends Foo
    

    请注意我们如何将FooSerializer 格式硬编码为write。现在我们可以序列化了

    println(X.write)
    println(Y.write)
    

    应该输出哪个

    "x"
    "y"
    

    方法3:在org.json4s.DefaultFormats旁边提供自定义DefaultFormats

    我们也可以尝试在我们自己的包中定义自定义DefaultFormats,像这样

    package example
    
    object DefaultFormats extends DefaultFormats {
      override val customSerializers: List[Serializer[_]] = List(FooSerializer)
    }
    

    这将允许我们像这样序列化 ADT

    import example.DefaultFormats
    implicit val formats = DefaultFormats
    case class Bar(foo: Foo)
    println(Serialization.write(Bar(X)))
    println(Serialization.write(X))
    println(Serialization.write(Y))
    

    应该输出哪个

    {"foo":"x"}
    "x"
    "y"
    

    拥有两种默认格式,org.json4s.DefaultFormatsexample.DefaultFormats,至少会让用户不得不在两者之间进行选择,如果说他们使用 IDE 自动导入它们。

    【讨论】:

    • write 方法非常适合转换单个值,但我认为它不适用于嵌套值(如果我有 case class Bar(foo: Foo),它应该序列化为 {"foo": "x"},而不是{"foo": {}}),仍然需要记住使用 write 函数。感谢您发现问题!很遗憾,它已经开放了将近 6 年。
    • 我已经使用 Scalastyle 方法编辑了答案,这将允许嵌套 ADT 序列化以及使用户能够继续使用write
    • 不错; scalastyle 方法很有趣!我认为这是一个合理的解决方法,但我不是一个超级粉丝,因为它确实需要使用库的任何人进行设置(但至少这只是一次)。方法 3 与方法 1 有何不同?似乎它只是指定适当的DefaultFormats 对象(作为object 而不是val)的另一种方法,并且仍然需要某种强制执行(例如通过scalastyle)?
    最近更新 更多