杰克逊,反序列化具有私有字段和无注释参数构造函数的类

19
有没有可能使用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来说很好,但是Jackson会抛出以下异常:

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似乎跳过了参数构造函数,因此必须使用反射在幕后创建一个无参构造函数。此外,存在一个Kotlin Jackson模块,可以为Kotlin数据类执行此操作,即使没有"-parameters"编译器标志。因此,Java Jackson似乎不存在这样的解决方案。这是Kotlin Jackson中可用的(美观而干净的)解决方案,我认为它也应该通过自定义模块在Java Jackson中变得可用。
val mapper = ObjectMapper()
    .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
    .registerModule(KotlinModule())     
        
val person: Person = mapper.readValue(json, Person::class.java)

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

13

使用混入注释的解决方案

您可以使用混入注释。当无法修改类时,这是一个很好的替代方案。您可以将其视为在运行时以面向方面的方式添加更多注释,以增强静态定义的注释。

假设您的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);

1
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>

对于 Gradle:
compileJava {
  options.compilerArgs << "-parameters"
}

OP已经提到了这些选项,他们不想要它,可能是因为他们没有运行Java 8。 - user1803551
@user1803551提到了Kotlin类的这个选项。 - Beno
@user1803551 他说:“所以很奇怪,对于Java Jackson似乎不存在这样的解决方案。” 我认为他正在寻找这个解决方案。如果你不想创建自定义反序列化器,Jackson没有支持另一种解决问题的方式。 - Beno
他说:“所以我期望在Jackson中必须有某种方式,而不需要修改Person类,也不需要Java 8和显式的自定义反序列化器。有人知道解决方案吗?”你的解决方案需要Java 8。 - user1803551
Java 8 的问题并不是 Java 8 本身,而是需要“-parameters”选项。解决方案需要1)重新编译类(可能是代码库之外的类),以及2)IntelliJ 忽略 Maven 编译器标志。https://youtrack.jetbrains.com/v2/issue/IDEA-143742 - Devabc
1
@Devabc 我知道,当你使用“-parameters”选项时,你必须通过mvn compile编译类文件。如果你的类在外部,你必须创建自定义的deserializermixin - Beno

0
由于没有默认构造函数,jackson或gson想要自己创建实例。您应该通过提供自定义反序列化程序来告诉API如何创建这样的实例。
以下是代码片段:
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());

2
代码在Person person = (Person) mapper.readValue(node.toString(), Person.class);处出现错误:无法构造`jacksonParametersTest.Person`的实例。这是一种自定义反序列化器解决方案,而我正在寻找一种没有自定义反序列化器的解决方案。Gson可以默认执行此操作。Jackson的Kotlin模块也可以执行此操作:https://github.com/FasterXML/jackson-module-kotlin 。我还发现Gson跳过了参数构造函数,因此它在幕后创建了一个默认的无参构造函数。 - Devabc
问题明确指出“不修改Person类,不使用Java 8,并且没有显式的自定义反序列化器”。 - E-Riz

-2

只有在您可以更改类实现的情况下,以下解决方案才有效

一个简单的解决方案就是在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提到了:“不修改类”。 - cassiomolin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接