如何使用Jackson执行自定义JSON反序列化,以尊重自定义注释?

huangapple go评论96阅读模式
英文:

How to perform a custom JSON deserialization with Jackson that respects a custom annotation?

问题

以下是您要翻译的内容的翻译部分:

我正在尝试将JSON反序列化为一个自定义的POJO,但我无法修改它。该POJO具有来自不同自定义内部序列化框架的注释,我无法使用该框架。如何创建一个自定义反序列化器,以便尊重这些注释?

以下是一个示例POJO:

public class ExampleClass {
    @Property(name = "id")
    public String id;

    @Property(name = "time_windows")
    @NotNull
    public List<TimeWindow> timeWindows = new ArrayList<>();

    public static class TimeWindow {
        @Property(name = "start")
        public Long start;

        @Property(name = "end")
        public Long end;
    }
}

因此,在这种情况下,反序列化器将查找与Property注释对应的JSON字段,并使用该注释中的值来决定要提取的字段。如果属性没有Property注释,则应将其忽略。

我一直在查阅Jackson文档,但尚未找到确切需要的内容。这是一个需要使用AnnotationIntrospector或可能是ContextualDeserializer的地方吗?

非常感谢您提供的任何指向正确方向的指导!

更新:我尝试实现了评论中的建议,但没有成功。

以下是我的注释检查器的初始实现:

class CustomAnnotationInspector : JacksonAnnotationIntrospector () {
    override fun hasIgnoreMarker(m: AnnotatedMember?): Boolean {
        val property = m?.getAnnotation(Property::class.java)
        return property == null
    }

    override fun findNameForDeserialization(a: Annotated?): PropertyName {
        val property = a?.getAnnotation(Property::class.java)
        return if (property == null) {
            super.findNameForDeserialization(a)
        } else {
            PropertyName(property.name)
        }
    }
}

这是我实际使用它的地方:

// 创建请求对象的空实例。
val paramInstance = nonPathParams?.type?.getDeclaredConstructor()?.newInstance()

// 创建一个新的对象映射器,将值从JSON写入空对象。
val mapper = ObjectMapper()

// 告诉映射器尊重自定义注释。
mapper.setAnnotationIntrospector(CustomAnnotationInspector())

// 将请求体的内容写入StringWriter(这对于mapper.writeValue方法是必需的)
val sw = StringWriter()
sw.write(context.bodyAsString)

// 反序列化StringWriter的内容到空POJO中。
mapper.writeValue(sw, paramInstance)

不幸的是,似乎从不调用findNameForDeserialization,并且没有将任何JSON值写入paramInstance。有人能看出我错在哪里吗?

谢谢!

更新2:我稍微更改了代码,现在能够识别属性名称,但Jackson无法创建对象实例。

以下是我的新代码:

val mapper = ObjectMapper()

// 告诉映射器尊重CoreNg注释。
val introspector = CustomAnnotationInspector()
mapper.setAnnotationIntrospector(introspector)

val paramInstance = mapper.readValue(context.bodyAsString,nonPathParams?.type)

我的自定义注释检查器中的断点被触发。但我收到以下异常:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `app.employee.api.employee.BOUpsertEmployeeRequest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

就我所知,它具有默认构造函数。有人知道问题是什么吗?

谢谢!

英文:

I am trying to deserialize JSON into a custom POJO that I am not able to modify. That POJO has annotations from a different custom internal serialization framework that I'm not able to use. How can I create a custom deserializer that will respect these annotations?

Here is an example POJO:

public class ExampleClass {
    @Property(name = "id")
    public String id;

    @Property(name = "time_windows")
    @NotNull
    public List<TimeWindow> timeWindows = new ArrayList<>();

    public static class TimeWindow {
        @Property(name = "start")
        public Long start;

        @Property(name = "end")
        public Long end;
    }
}

So in this case, the deserializer would look for fields in the JSON that correspond to the Property annotations, and use the value in that annotation to decide what field to grab. If a property doesn't have the Property annotation, it should be ignored.

I have been going through the Jackson docs but haven't been able to find exactly what I need. Is this a place where an AnnotationIntrospector would be useful? Or possibly a ContextualDeserializer?

Any pointers in the right direction would be greatly appreciated!


Update: I tried implementing the advice in the comments, but without success.

Here is my initial implementation of the introspector:

class CustomAnnotationInspector : JacksonAnnotationIntrospector () {
    override fun hasIgnoreMarker(m: AnnotatedMember?): Boolean {
        val property = m?.getAnnotation(Property::class.java)
        return property == null
    }

    override fun findNameForDeserialization(a: Annotated?): PropertyName {
        val property = a?.getAnnotation(Property::class.java)
        return if (property == null) {
            super.findNameForDeserialization(a)
        } else {
            PropertyName(property.name)
        }
    }
}

And here is where I actually use it:

// Create an empty instance of the request object.
val paramInstance = nonPathParams?.type?.getDeclaredConstructor()?.newInstance()

// Create new object mapper that will write values from
// JSON into the empty object.
val mapper = ObjectMapper()

// Tells the mapper to respect custom annotations.
mapper.setAnnotationIntrospector(CustomAnnotationInspector())

// Write the contents of the request body into the StringWriter
// (this is required for the mapper.writeValue method
val sw = StringWriter()
sw.write(context.bodyAsString)

// Deserialize the contents of the StringWriter
// into the empty POJO.
mapper.writeValue(sw, paramInstance)

Unfortunately it seems that findNameForDeserialization is never called, and none of the JSON values are written into paramInstance. Can anybody spot where I'm going wrong?

Thank you!


Update 2: I changed the code slightly, I'm now able to identify the property names but Jackson is failing to create an instance of the object.

Here's my new code:

val mapper = ObjectMapper()

// Tells the mapper to respect CoreNg annotations.
val introspector = CustomAnnotationInspector()
mapper.setAnnotationIntrospector(introspector)

val paramInstance = mapper.readValue(context.bodyAsString,nonPathParams?.type)

My breakpoints in the custom annotation introspector are getting hit. But I'm getting the following exception:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `app.employee.api.employee.BOUpsertEmployeeRequest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

Here is the POJO I'm trying to deserialize:

public class BOUpsertEmployeeRequest {
    public BOUpsertEmployeeRequest () { }

    @NotNull
    @Property(name = "xref_code")
    public String xrefCode;

    @Property(name = "first_name")
    public String firstName;

    @Property(name = "last_name")
    public String lastName;

    @Property(name = "email_address")
    public String emailAddress;

    @Property(name = "phone")
    public String phone;

    @Property(name = "address")
    public List<String> address;

    @Property(name = "employment_status")
    public String employmentStatus;

    @Property(name = "pay_type")
    public String payType;

    @Property(name = "position")
    public String position;

    @Property(name = "skills")
    public List<String> skills;

    @Property(name = "gender")
    public String gender;
}

As far as I can tell it has a default constructor. Anybody have any idea what the problem is?

Thank you!

答案1

得分: 3

hasIgnoreMarker 方法不仅用于 字段,还用于 构造函数,包括虚拟构造函数:

调用此方法以检查给定属性是否标记为要忽略。这用于确定是否要在每个属性基础上忽略属性,通常结合来自多个访问器(getter、setter、字段、构造函数参数)的注释。

在这种情况下,您应该只忽略未正确标记的 字段

static class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
    @Override
    public PropertyName findNameForDeserialization(Annotated a) {
        Property property = a.getAnnotation(Property.class);
        if (property == null) {
            return PropertyName.USE_DEFAULT;
        } else {
            return PropertyName.construct(property.name());
        }
    }

    @Override
    public boolean hasIgnoreMarker(AnnotatedMember m) {
        return m instanceof AnnotatedField
                && m.getAnnotation(Property.class) == null;
    }
}

示例:

class Pojo {
//    @Property(name = "id")
    Integer id;
//    @Property(name = "number")
    Integer number;
    @Property(name = "assure")
    Boolean assure;
    @Property(name = "person")
    Map<String, String> person;
}
String json =
        "{\"id\" : 1, \"number\" : 12345, \"assure\" : true," +
        " \"person\" : {\"name\" : \"John\", \"age\" : 23}}";

ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new CustomAnnotationIntrospector());
Pojo pojo = mapper.readValue(json, Pojo.class);

System.out.println(pojo);
Pojo{id=null, number=null, assure=true, person={name=John, age=23}}

注意: 自定义的 Property 注释应具有 RetentionPolicy.RUNTIME(与 JsonProperty 注释相同):

@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
    String name();
}
英文:

Method hasIgnoreMarker is called not only for fields, but also for the constructor, including the virtual one:

> Method called to check whether given property is marked to be ignored. This is used to determine whether to ignore properties, on per-property basis, usually combining annotations from multiple accessors (getters, setters, fields, constructor parameters).

In this case you should ignore only fields, that are not marked properly:

static class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
    @Override
    public PropertyName findNameForDeserialization(Annotated a) {
        Property property = a.getAnnotation(Property.class);
        if (property == null) {
            return PropertyName.USE_DEFAULT;
        } else {
            return PropertyName.construct(property.name());
        }
    }

    @Override
    public boolean hasIgnoreMarker(AnnotatedMember m) {
        return m instanceof AnnotatedField
                && m.getAnnotation(Property.class) == null;
    }
}

Example:

class Pojo {
//    @Property(name = "id")
    Integer id;
//    @Property(name = "number")
    Integer number;
    @Property(name = "assure")
    Boolean assure;
    @Property(name = "person")
    Map<String, String> person;
}
String json =
        "{\"id\" : 1, \"number\" : 12345, \"assure\" : true," +
        " \"person\" : {\"name\" : \"John\", \"age\" : 23}}";

ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new CustomAnnotationIntrospector());
Pojo pojo = mapper.readValue(json, Pojo.class);

System.out.println(pojo);
Pojo{id=null, number=null, assure=true, person={name=John, age=23}}

Note: Custom Property annotation should have RetentionPolicy.RUNTIME (same as JsonProperty annotation):

@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
    String name();
}

答案2

得分: 0

以下是您要求的翻译部分:

我建议采用不同的方法

在运行时使用字节码仪器库[Byte Buddy][1]及其Java代理在字段上重新注释为适当的Jackson注解只需通过反射实现逻辑请参考以下示例

```java
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.dynamic.DynamicType.Builder.FieldDefinition.Valuable;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.matcher.ElementMatchers;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyJsonIgnore {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyJsonProperty {
  String name();
}
public class Sample {
  public static void main(String[] args) throws JsonProcessingException {
    ByteBuddyAgent.install();
    ClassReloadingStrategy classReloadingStrategy = ClassReloadingStrategy.fromInstalledAgent();
    ByteBuddy byteBuddy = new ByteBuddy();
    AnnotationDescription jsonIgnoreDesc =
        AnnotationDescription.Builder.ofType(JsonIgnore.class).build();

    Builder<Person> personBuilder = byteBuddy.redefine(Person.class);

    for (Field declaredField : Person.class.getDeclaredFields()) {
      Valuable<Person> field = personBuilder.field(ElementMatchers.named(declaredField.getName()));

      MyJsonProperty myJsonProperty = declaredField.getAnnotation(MyJsonProperty.class);
      if (myJsonProperty != null) {
        AnnotationDescription jsonPropertyDesc =
            AnnotationDescription.Builder.ofType(JsonProperty.class)
                .define("value", myJsonProperty.name())
                .build();
        personBuilder = field.annotateField(jsonPropertyDesc);
      }

      MyJsonIgnore myJsonIgnore = declaredField.getAnnotation(MyJsonIgnore.class);
      if (myJsonIgnore != null) {
        personBuilder = field.annotateField(jsonIgnoreDesc);
      }
    }

    personBuilder.make().load(Sample.class.getClassLoader(), classReloadingStrategy);

    Person person = new Person("Utku", "Ozdemir", "Berlin");
    
    ObjectMapper objectMapper = new ObjectMapper();
    String jsonString = objectMapper.writeValueAsString(person);
    System.out.println(jsonString);
  }
}
class Person {
  @MyJsonProperty(name = "FIRST")
  private String firstName;

  @MyJsonProperty(name = "LAST")
  private String lastName;

  @MyJsonIgnore private String city;

  public Person(String firstName, String lastName, String city) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.city = city;
  }
}

在上面的示例中,我

  • 创建了MyJsonPropertyMyJsonIgnore注解以及Person类,供演示目的使用
  • 使用Byte Buddy代理仪器来对当前的Java进程进行字节码处理
  • 创建了一个Byte Buddy生成器来重新定义Person
  • 循环遍历Person类的字段并检查这些注解
  • 为每个字段添加了额外的注解(在生成器上),包括Jackson的JsonProperty(具有正确的字段名称映射)和JsonIgnore
  • 处理完字段后,生成新类的字节码并使用Byte Buddy代理的类重新加载机制加载到当前类加载器中
  • 将人员对象写入标准输出。

它按预期打印出:

{"FIRST":"Utku","LAST":"Ozdemir"}

(字段city被忽略)

这个解决方案可能会感觉有点复杂,但另一方面,它是一个相当通用的解决方案 - 通过稍微更改逻辑,您可以处理所有的第三方类(无法修改的类),而不是逐个处理它们。


<details>
<summary>英文:</summary>
I will suggest a different approach: 
In the runtime, with the bytecode instrumentation library [Byte Buddy][1] and its Java agent, re-annotate the fields with the proper Jackson Annotations. Simply implement the logic via reflection. See the following example:
```java
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.dynamic.DynamicType.Builder.FieldDefinition.Valuable;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.matcher.ElementMatchers;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyJsonIgnore {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyJsonProperty {
  String name();
}
public class Sample {
  public static void main(String[] args) throws JsonProcessingException {
    ByteBuddyAgent.install();
    ClassReloadingStrategy classReloadingStrategy = ClassReloadingStrategy.fromInstalledAgent();
    ByteBuddy byteBuddy = new ByteBuddy();
    AnnotationDescription jsonIgnoreDesc =
        AnnotationDescription.Builder.ofType(JsonIgnore.class).build();

    Builder&lt;Person&gt; personBuilder = byteBuddy.redefine(Person.class);

    for (Field declaredField : Person.class.getDeclaredFields()) {
      Valuable&lt;Person&gt; field = personBuilder.field(ElementMatchers.named(declaredField.getName()));

      MyJsonProperty myJsonProperty = declaredField.getAnnotation(MyJsonProperty.class);
      if (myJsonProperty != null) {
        AnnotationDescription jsonPropertyDesc =
            AnnotationDescription.Builder.ofType(JsonProperty.class)
                .define(&quot;value&quot;, myJsonProperty.name())
                .build();
        personBuilder = field.annotateField(jsonPropertyDesc);
      }

      MyJsonIgnore myJsonIgnore = declaredField.getAnnotation(MyJsonIgnore.class);
      if (myJsonIgnore != null) {
        personBuilder = field.annotateField(jsonIgnoreDesc);
      }
    }

    personBuilder.make().load(Sample.class.getClassLoader(), classReloadingStrategy);

    Person person = new Person(&quot;Utku&quot;, &quot;Ozdemir&quot;, &quot;Berlin&quot;);
    
    ObjectMapper objectMapper = new ObjectMapper();
    String jsonString = objectMapper.writeValueAsString(person);
    System.out.println(jsonString);
  }
}
class Person {
  @MyJsonProperty(name = &quot;FIRST&quot;)
  private String firstName;

  @MyJsonProperty(name = &quot;LAST&quot;)
  private String lastName;

  @MyJsonIgnore private String city;

  public Person(String firstName, String lastName, String city) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.city = city;
  }
}

In the example above, I

  • create MyJsonProperty and MyJsonIgnore annotations and a Person class for the demonstration purpose
  • instrument the current Java process with the Byte buddy agent
  • create a bytebuddy builder to redefine the Person class
  • loop over the fields of the Person class and check for these annotations
  • add an additional annotation to each of those fields (on the builder), Jackson's JsonProperty (with the correct field name mapping) and JsonIgnore.
  • after being done with the fields, make the new class bytecode and load it to the current classloader using the byte buddy agent's class reloading mechanism
  • write a person object to the stdout.

It prints, as expected:

{&quot;FIRST&quot;:&quot;Utku&quot;,&quot;LAST&quot;:&quot;Ozdemir&quot;}

(the field city is ignored)

This solution might feel like an overkill, but on the other side, it is pretty generic solution - with a few changes in the logic, you could handle all the 3rd party classes (which you are not able to modify) instead of handling them case by case.

huangapple
  • 本文由 发表于 2020年8月12日 00:38:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/63362602.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定