使用 Jackson 来(反)序列化 Scala 案例类

Posted

技术标签:

【中文标题】使用 Jackson 来(反)序列化 Scala 案例类【英文标题】:Using Jackson to (De)-serialize a Scala Case Class 【发布时间】:2015-03-31 23:29:35 【问题描述】:

我使用 Jackson 测试了 Scala 案例类的序列化。

DeserializeTest.java

    public static void main(String[] args) throws Exception  // being lazy to catch-all

        final ObjectMapper mapper          = new ObjectMapper();
        final ByteArrayOutputStream stream = new ByteArrayOutputStream();

        mapper.writeValue(stream, p.Foo.personInstance());

        System.out.println("result:" +  stream.toString());
    

Foo.scala

object Foo 
  case class Person(name: String, age: Int, hobbies: Option[String])
  val personInstance = Person("foo", 555, Some("things"))
  val PERSON_JSON = """  "name": "Foo", "age": 555  """

当我运行上面main的Java类时,抛出了异常:

[error] Exception in thread "main" org.codehaus.jackson.map.JsonMappingException: 
 No serializer found for class p.Foo$Person and no properties discovered 
 to create BeanSerializer (to avoid exception, 
 disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS) )

如何(反)序列化 Scala 案例类?

【问题讨论】:

Jackson 有几个很好的 Scala 包装器;你至少应该考虑使用一个。 Play-json 恰好是我使用的。好的 (IMO) 使用基于类型类的反序列化/序列化,您可以在其中将成员添加到案例类的伴随对象中,以告诉它如何来回映射到 JSON,并为此提供很好的习惯用法。 【参考方案1】:

Jackson 期望您的类是 JavaBean,这意味着它期望该类的每个属性都有一个 getX() 和/或 setX()。

选项 1

您可以在 Scala 中使用注解 BeanProperty 创建 JavaBean 类。

例子

case class Person(
   @BeanProperty val name: String, 
   @BeanProperty val age: Int, 
   @BeanProperty val hobbies: Option[String]
)

在这种情况下,val 意味着只定义了一个 getter。如果你想要反序列化的设置器,你将属性定义为 var。

选项 2

虽然选项 1 会起作用,但如果你真的想使用 Jackson,那么有一些包装器可以让它处理像 FasterXML's scala module 这样的 Scala 类,这可能是一种更好的方法。我没有使用它,因为我一直在使用内置的 Json 库来玩。

【讨论】:

对于选项 1,您需要在案例类中添加一个无参数构造函数,如下所示: def this() = this("",0,None) 因为距离最初的问题已经过去五年了,我应该补充一点,答案可能有以前工作过,但我现在将 Scala 2.13.4jackson-module-scala 2.11.3jackson-dataformat-csv 2.11.3 一起使用,这是必要的,否则我会收到消息 "Cannot construct instance of `Person` (no Creators, like default constructor, exist)"【参考方案2】:

找到了一个适用于 jackson 和 scala 案例类的解决方案。

我为 jackson 使用了一个 scala 模块 - jackson-module-scala。

libraryDependencies ++= Seq(
 "com.fasterxml.jackson.core" % "jackson-databind" % "2.5.3",
 "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.2.2"
)

我必须使用 @JsonProperty 注释我的案例类中的字段。

这是我的案例类的样子:

case class Person(@JsonProperty("FName") FName: String, @JsonProperty("LName") LName: String)

这就是我反序列化的方式:

val objectMapper = new ObjectMapper() with ScalaObjectMapper
objectMapper.registerModule(DefaultScalaModule)
val str = """"FName":"Mad", "LName": "Max""""
val name:Person = objectMapper.readValue[Person](str)

序列化更容易:

val out = new ByteArrayOutputStream()
objectMapper.writeValue(out, name)
val json = out.toString

想澄清一下我正在使用

com.fasterxml.jackson.databind.ObjectMapper

在这个问题中,他似乎在使用

org.codehaus.jackson.map.ObjectMapper 

这不适用于 ScalaObjectMapper。

【讨论】:

您的解决方案没有涉及“爱好:Option[String]”属性,我认为他的部分问题在于将 json String 属性作为 Option[String] 处理。 Jackson 需要知道如何将 Option[String] 解释为 Some[String] 或 None 几点注意事项:1)@JsonProperty 在案例类尤其是不必要的。当属性名称与字段名称相同时。 2) 您应该尝试使用与 jackson-core 相同版本的 jackson-module-scala。 3) Jackson 2.0 被重新打包为 com.fasterxml,但在其他方面与 Jackson 1.0 一样好(如果不是更好的话)。【参考方案3】:

根据 Priyank Desai 的回答,我创建了一个通用函数来将 json string 转换为 case class

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper

def jsonToType[T](json:String)(implicit m: Manifest[T]) :T = 
   val objectMapper = new ObjectMapper() with ScalaObjectMapper
   objectMapper.registerModule(DefaultScalaModule)
   objectMapper.readValue[T](json)

用法:

case class Person(@JsonProperty("name") Name:String, @JsonProperty("age") Age:Int)

val personName = jsonToType[Person](jsonString).name

【讨论】:

清晰有效的答案。只是一个小错字 - 参数 name 而不是 Name【参考方案4】:

我创建了一个通用函数来转换JSON String to Case Class/ObjectCase Class/Object to JSON String

请找到我使用泛型 here 提供的有效且详细的答案。

【讨论】:

【参考方案5】:

使用 upickle 反序列化对象比使用 Jackson 容易得多。这是一个例子:

case class City(name: String, funActivity: String, latitude: Double)
implicit val cityRW = upickle.default.macroRW[City]
val str = """"name":"Barcelona","funActivity":"Eat tapas","latitude":41.39"""
val barcelona = upickle.default.read[City](str)

杰克逊的解决方案更加冗长。请参阅此post 以了解有关此方法的更多信息。

【讨论】:

【参考方案6】:

如果您使用的是 Spring MVC,请使用如下配置:

import java.util.List

@Configuration
@EnableWebMvc
class WebConfig extends WebMvcConfigurer 
  
  override def configureMessageConverters(converters: List[HttpMessageConverter[_]]): Unit = 
    val conv = new MappingJackson2HttpMessageConverter();
    val mapper = JsonMapper.builder()
      .addModule(DefaultScalaModule)
      .build();
    conv.setObjectMapper(mapper);
    converters.add(conv);
  

然后序列化/反序列化普通案例类和集合类型将按预期工作。

【讨论】:

以上是关于使用 Jackson 来(反)序列化 Scala 案例类的主要内容,如果未能解决你的问题,请参考以下文章

在scala中使用jackson反序列化json

Jackson关于Boolean类型反序列化问题

使用 Jackson 自定义反序列化:扩展默认反序列化器

Jackson序列化(8)— 支持泛型的反序列化

使用 Jackson 将嵌套数组反序列化为 ArrayList

使用 Jackson 将 JSON 反序列化为 ArrayList<POJO>