杰克逊,反序列化具有私有字段的类和没有注释的 arg-constructor

Posted

技术标签:

【中文标题】杰克逊,反序列化具有私有字段的类和没有注释的 arg-constructor【英文标题】:Jackson, deserialize class with private fields and arg-constructor without annotations 【发布时间】:2018-05-14 05:35:56 【问题描述】:

是否可以使用 Jackson 反序列化为具有私有字段和自定义参数构造函数的类而不使用注释和修改类?

我知道在 Jackson 中使用这种组合是可能的:1) Java 8,2) 使用“-parameters”选项编译,3) 参数名称与 JSON 匹配。但在 GSON 中也有可能默认情况下没有所有这些限制。

例如:

public class Person 
    private final String firstName;
    private final String lastName;
    private final int age;

    public Person(String firstName, String lastName, int age) 
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    

    public static void main(String[] args) throws IOException 
        String json = "firstName: \"Foo\", lastName: \"Bar\", age: 30";
        
        System.out.println("GSON: " + deserializeGson(json)); // works fine
        System.out.println("Jackson: " + deserializeJackson(json)); // error
    

    public static Person deserializeJackson(String json) throws IOException 
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        return mapper.readValue(json, Person.class);
    

    public static Person deserializeGson(String json) 
        Gson gson = new GsonBuilder().create();
        return gson.fromJson(json, Person.class);
    

这对 GSON 很有效,但杰克逊会抛出:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `jacksonParametersTest.Person` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"firstName: "Foo", lastName: "Bar", age: 30"; line: 1, column: 2]
    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)

这在 GSON 中是可能的,所以我希望在 Jackson 中必须有某种方法,而无需修改 Person 类,无需 Java 8,也无需显式自定义反序列化器。有人知道解决办法吗?

更新,附加信息

Gson 似乎跳过了参数构造函数,所以它必须在幕后使用反射创建一个无参数构造函数。

此外,即使没有“-parameters”编译器标志,也有一个 Kotlin Jackson module 能够为 Kotlin 数据类执行此操作。 所以奇怪的是,Java Jackson 似乎不存在这样的解决方案。

这是 Kotlin Jackson 中可用的(又好又干净的)解决方案(IMO 也应该通过自定义模块在 Java Jackson 中可用):

val mapper = ObjectMapper()
    .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
    .registerModule(KotlinModule())     
        
val person: Person = mapper.readValue(json, Person::class.java)

【问题讨论】:

person创建一个空的构造函数 但这需要修改类。我的问题是这是否可能不修改它,就像 Gson 一样。 (我会在我的问题中更明确地说明这一点。) 所以你不想注释类,你不想使用参数名称解​​决方案,你不想自定义反序列化器。在我尝试写答案之前还有什么其他的吗? 不是真的,我也没有对问题添加新的要求。情况基本上是:您的代码库之外有大量不可变的类,如何在没有样板代码的情况下有效地反序列化它们。解决方案可能在于尚不存在的自定义 Jackson 模块,或者使用“-parameter”标志。 (这个标志很麻烦,因为 1)它需要重新编译和 2)IntelliJ 忽略 Maven 编译器标志。但我已接受 Kotlin Jackson 模块作为我项目的解决方案。 @Kalaiselvan,我赞成您的评论,因为它对我的同事有用。但是我没有提供默认/空构造函数,我仍然能够解决它。唯一的区别是我使用的是 Windows 10,而他使用的是 Mac。 【参考方案1】:

混合注释的解决方案

您可以使用混合注释。当修改类不是一个选项时,这是一个很好的选择。您可以将其视为一种在运行时添加更多注释的面向方面的方式,以增加静态定义的注释。

假设您的Person 类定义如下:

public class Person 

    private final String firstName;
    private final String lastName;
    private final int age;

    public Person(String firstName, String lastName, int age) 
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    

    // Getters omitted

先定义一个混入注解抽象类:

public abstract class PersonMixIn 

    PersonMixIn(@JsonProperty("firstName") String firstName,
                @JsonProperty("lastName") String lastName,
                @JsonProperty("age") int age) 
    

然后配置 ObjectMapper 使用定义的类作为你的 POJO 的混合:

ObjectMapper mapper = new ObjectMapper();
mapper.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
mapper.addMixIn(Person.class, PersonMixIn.class);

并反序列化 JSON:

String json = "firstName: \"Foo\", lastName: \"Bar\", age: 30";
Person person = mapper.readValue(json, Person.class);

【讨论】:

【参考方案2】:

由于没有默认构造函数,jackson 或 gson 希望自己创建实例。你应该告诉 API 如何通过提供来创建这样的实例 自定义反序列化。

这里是一个sn-p代码

public class PersonDeserializer extends StdDeserializer<Person>  
    public PersonDeserializer() 
        super(Person.class);
     

    @Override
    public Person deserialize(JsonParser jp, DeserializationContext ctxt) 
            throws IOException, JsonProcessingException 
        try 
            final JsonNode node = jp.getCodec().readTree(jp);
            final ObjectMapper mapper = new ObjectMapper();
            final Person person = (Person) mapper.readValue(node.toString(),
                    Person.class);
            return person;
         catch (final Exception e) 
            throw new IOException(e);
        
    

然后注册简单的模块来处理你的类型

final ObjectMapper mapper = jacksonBuilder().build();
SimpleModule module = new SimpleModule();
module.addDeserializer(Person.class, new PersonDeserializer());

【讨论】:

代码在Person person = (Person) mapper.readValue(node.toString(), Person.class); 出现错误:Cannot construct instance of `jacksonParametersTest.Person` 这也是一个自定义解串器解决方案,而我正在寻找一个没有的。默认情况下,Gson 能够做到这一点。 Jackson 的 Kotlin 模块也可以做到这一点:github.com/FasterXML/jackson-module-kotlin。我还发现 Gson 跳过了参数构造函数,所以它在幕后创建了一个默认的无参数 ctor。【参考方案3】:

Jackson 提供模块jackson-modules-java8 来解决您的问题。

您必须按照以下方式构建您的ObjectMapper

ObjectMapper mapper = new ObjectMapper()
        .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
        .registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES));

您必须添加-parameters 作为编译器参数。

maven 示例:

 <plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-compiler-plugin</artifactId>
       <version>3.5.1</version>
       <configuration>
           <!--somecode-->

           <compilerArgument>-parameters</compilerArgument>

       </configuration>
 </plugin>

对于毕业:

compileJava 
  options.compilerArgs << "-parameters"

【讨论】:

OP 已经提到了这个选项并且不想要它,可能他们不运行 Java 8。 @user1803551 他提到了 Kotlin 类的这个选项。 @user1803551 他说“奇怪的是,Java Jackson 似乎不存在这样的解决方案。”在我看来,他搜索了这个解决方案。如果您不想创建自定义反序列化器,Jackson 不支持另一种解决问题的方法。 他说 “所以我希望在 Jackson 中必须有某种方法,而无需修改 Person 类、Java 8 和显式自定义反序列化器。有人知道解决方案吗?” 您的解决方案需要 Java 8。 @Devabc 我知道,当您使用“-parameters”时,您必须通过mvn compile 编译类,如果您的类在外部,您必须创建自定义deserializermixin跨度> 【参考方案4】:

仅当您可以更改类实现时,以下解决方案才有效

一个简单的解决方案就是创建一个默认构造函数

Person()Person 类中

public class Person 
    private final String firstName;
    private final String lastName;
    private final int age;

    public Person() 
    

    public Person(String firstName, String lastName, int age) 
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    
    ...

【讨论】:

嗯...这是一个显而易见的解决方案。但是OP提到:“不修改类”

以上是关于杰克逊,反序列化具有私有字段的类和没有注释的 arg-constructor的主要内容,如果未能解决你的问题,请参考以下文章

杰克逊的@JsonSubTypes仍然是多态反序列化所必需的吗?

使用杰克逊的 JSON 反序列化:没有找到适合类型的构造函数 - 可以提供默认构造函数或注释构造函数 [重复]

ZonedDateTime 的杰克逊反序列化问题

Protobuf-net / NetCore2:反序列化忽略带注释的私有字段

杰克逊:反序列化为每个值都具有正确类型的 Map<String, Object>

如何指定杰克逊只使用字段 - 最好是全局