【问题标题】:JsonDeserializer does not work on the Class but only on the single element of the classJsonDeserializer 不适用于类,但仅适用于类的单个元素
【发布时间】:2021-06-10 08:25:14
【问题描述】:

我创建了一个新的反序列化器,以便能够将空字符串写为 null

public class CustomDeserializer extends JsonDeserializer<String> {
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException  {
        JsonNode node = jsonParser.readValueAsTree();
        if (node.asText().isEmpty()) {
            return null;
        }
        return node.toString();
    }
}

尝试在每个用户字段上进行单个注释,自定义工作但通过在整个类上插入注释,我无法再打印 Json 消息

@JsonDeserialize(using = CustomDeserializer.class)
public class User {
    private String firstName;
    private String lastName;
    private String age;
    private String address; }

CustomExceptionHandler 向我抛出此错误:Class MethodArgumentNotValidException 这是我的 Kafka 消费者,我唯一输入了验证注释的地方,但即使删除它也会给我同样的错误

public class KafkaConsumer {

    @Autowired
    private UserRepository userRepository;

    @KafkaListener(topics = "${spring.kafka.topic.name}")
    public void listen(@Validated User user) {

        User  user = new User(user);
        UserRepository.save(user.getName(), user);
    }
}

对象映射器

public ObjectMapper getObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.setSerializationInclusion(Include.NON_NULL);
    return mapper;
}

是否有可能让它在全班都使用?

【问题讨论】:

  • 好吧,当然,在类上设置自定义反序列化器并不仅仅适用于字段。您将注释设置在您希望反序列化器吐出的类型上。您可以做什么:替换对象映射器实例中的标准字符串反序列化器或为您的类创建一个自定义反序列化器(这可以继承自其中一个标准反序列化器)并让该类直接或通过委托给您的自定义字符串来处理字符串属性解串器。
  • 嗨,Thomas,你能给我一个例子作为我可以创建的这个对象的答案吗?我不太明白如何设置它
  • 嗯,你已经扩展了一个标准反序列化器并使用它,你还需要什么?
  • 我不明白如何替换对象映射器实例中的标准字符串反序列化器。我还在问题中添加了 objectMapper
  • 嗯,ObjectMapper 是 Jackson 使用的中心类。如何更改在您的案例中使用的对象映射器取决于您的框架,我需要让您参考文档或其他搜索。替换标准反序列化器应该是使用对象映射器创建和注册SimpleModule 并调用 `module.addSerializer(String.class ,new CustomDeserializer());´ 或类似的东西。

标签: java json jackson json-deserialization


【解决方案1】:

如果您希望将代表整个对象的空String 视为null,您可以启用ACCEPT_EMPTY_STRING_AS_NULL_OBJECT Jackson deserialization feature,默认情况下禁用。

您可以在配置ObjectMapper 时包含它:

public ObjectMapper getObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.setSerializationInclusion(Include.NON_NULL);
    // Enable ACCEPT_EMPTY_STRING_AS_NULL_OBJECT deserialization feature
    mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
    return mapper;
}

如上所述,当您想将表示整个对象的空String视为null时很有用;但是,它不适用于 String 类型的单个属性:在后一种情况下,您可以安全地使用自定义反序列化器,因此,解决方案实际上是两种方法的混合,使用 ACCEPT_EMPTY_STRING_AS_NULL_OBJECT 反序列化功能来处理整个对象,以及用于处理单个 String 属性的自定义反序列化器。

请参阅 thisthis other 相关的 SO 问题。

您也可以改进您的自定义User 解串器。请考虑例如(为了清楚起见,我将名称重构为UserDeserializer):

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;

public class UserDeserializer extends JsonDeserializer<User> {

  @Override
  public User deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
    JsonNode node = jsonParser.readValueAsTree();
    Iterator<String> fieldNames = node.fieldNames();
    // Process Jackson annotations looking for aliases
    Map<String, String> fieldAliases = this.getAliases();
    User user = new User();
    boolean anyNonNull = false;
    // Iterate over every field. The deserialization process assume simple properties
    while(fieldNames.hasNext()) {
      String fieldName = fieldNames.next();
      JsonNode fieldValue = node.get(fieldName);
      String fieldValueTextRepresentation = fieldValue.asText();
      if (fieldValueTextRepresentation != null && !fieldValueTextRepresentation.trim().isEmpty()) {
        // Check if the field is aliased
        String actualFieldName = fieldAliases.get(fieldName);
        if (actualFieldName == null) {
          actualFieldName = fieldName;
        }

        this.setFieldValue(user, actualFieldName, fieldValueTextRepresentation);
        anyNonNull = true;
      }
    }

    return anyNonNull ? user : null;
  }

  // Set field value via Reflection
  private void setFieldValue(User user, String fieldName, String fieldValueTextRepresentation) {
    try {
      Field field = User.class.getDeclaredField(fieldName);
      Object fieldValue = null;
      Class clazz = field.getType();
      // Handle each class type: probably this code can be improved, but it is extensible and adaptable,
      // you can include as many cases as you need.
      if (clazz.isAssignableFrom(String.class)) {
        fieldValue = fieldValueTextRepresentation;
      } else if (clazz.isAssignableFrom(LocalDate.class)) {
        // Adjust the date pattern as required
        // For example, if you are receiving the information
        // like this: year-month-day, as in the provided example,
        // you can use the following pattern
        fieldValue = LocalDate.parse(fieldValueTextRepresentation, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
      } else if (clazz.isAssignableFrom(Integer.class)) {
        fieldValue = Integer.parseInt(fieldValueTextRepresentation);
      }
      field.setAccessible(true);
      field.set(user, fieldValue);
    } catch (Exception e) {
      // Handle the problem as appropriate
      e.printStackTrace();
    }
  }
  
  /* Look for Jackson aliases */
  private Map<String, String> getAliases() {
    Map<String, String> fieldAliases = new HashMap<>();

    Field[] fields = User.class.getDeclaredFields();
    for (Field field: fields) {
      Annotation annotation = field.getAnnotation(JsonAlias.class);
      if (annotation != null) {
        String fieldName = field.getName();
        JsonAlias jsonAliasAnnotation = (JsonAlias) annotation;
        String[] aliases = jsonAliasAnnotation.value();
        for (String alias: aliases) {
          fieldAliases.put(alias, fieldName);
        }
      }
    }

    return fieldAliases;
  }
}

有了这个序列化程序,给定一个User 类,类似于:

import java.time.LocalDate;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

@JsonDeserialize(using = UserDeserializer.class)
public class User {
  private String firstName;
  private String lastName;
  private Integer age;
  private String address;
  @JsonAlias("dateofbirth")
  private LocalDate dateOfBirth;

  // Setters and getters omitted for brevity

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    User user = (User) o;

    if (firstName != null ? !firstName.equals(user.firstName) : user.firstName != null) return false;
    if (lastName != null ? !lastName.equals(user.lastName) : user.lastName != null) return false;
    if (age != null ? !age.equals(user.age) : user.age != null) return false;
    if (address != null ? !address.equals(user.address) : user.address != null) return false;
    return dateOfBirth != null ? dateOfBirth.equals(user.dateOfBirth) : user.dateOfBirth == null;
  }

  @Override
  public int hashCode() {
    int result = firstName != null ? firstName.hashCode() : 0;
    result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
    result = 31 * result + (age != null ? age.hashCode() : 0);
    result = 31 * result + (address != null ? address.hashCode() : 0);
    result = 31 * result + (dateOfBirth != null ? dateOfBirth.hashCode() : 0);
    return result;
  }

以及以下 JSON(我更改为 dateofbirth 字段的名称只是为了测试别名):

{"firstName":"John","age":40,"dateofbirth":"1978-03-16"}

您应该获得适当的结果,考虑以下测试:

  public static void main(String... args) throws JsonProcessingException {
    User user = new User();
    user.setFirstName("John");
    user.setAge(40);
    user.setDateOfBirth(LocalDate.of(1978, Month.MARCH, 16));

    ObjectMapper mapper = new ObjectMapper();
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

    String json = "{\"firstName\":\"John\",\"age\":40,\"dateofbirth\":\"1978-03-16\"}";

    User reconstructed = mapper.readValue(json, User.class);

    System.out.println(user.equals(reconstructed));
  }

最后,请注意,为了让您的 @KafkaListener 处理 null 值,您必须将 @Payload 注释与 required = false 一起使用,例如:

public class KafkaConsumer {

    @Autowired
    private UserRepository userRepository;

    @KafkaListener(topics = "${spring.kafka.topic.name}")
    public void listen(@Payload(required = false) User user) {
        // Handle null value
        if (user == null) {
          // Consider logging the event
          // logger.debug("Null message received");
          System.out.println("Null message received");
          return;
        }

        // Continue as usual
        User  user = new User(user);
        UserRepository.save(user.getName(), user);
    }
}

查看相关的 Spring Kafka documentation 和这个 Github issue 以及相关的 commitThis SO question 也可能是相关的。

【讨论】:

  • 嗨,我已经尝试过你的答案,但它一直给我一个像这样的错误MethodArgumentNotValidException。我一遍又一遍地看到你为我列出的问题,但我还没有找到解决方案这个错误它产生了我
  • @Jacket 我用更多信息更新了答案。基本上,请您将您的方法注释为@Payload(required = false) User user,看看它是否有效?
  • 嗨,我尝试插入 @Payload 注释,确实有变化,现在错误是 NullPointException
  • 我很高兴听到@Jacket。对不起,当然,现在有必要处理空值。请参阅更新的答案。我希望它有所帮助。
  • @Jacket 有用吗?您是否能够测试建议的解决方案?
【解决方案2】:

CustomDeserializer 是为 String 类型定义的,它用于反序列化 User 对象。这就是为什么反序列化器在应用时处理单个 User 字段,而不是整个 User 对象的原因。为了在整个User 对象上应用解串器,CustomDeserializer 应该是User 类型。像这样的:

public class CustomDeserializer extends JsonDeserializer<User> {
@Override
public User deserialize(JsonParser jsonParser, DeserializationContext context) throws
    IOException {
    JsonNode node = jsonParser.readValueAsTree();
    String firstName = null;
    String lastName = null;
    String age = null;
    String address = null;
    if(node.has("firstName") && !node.get("firstName").asText().isEmpty()) {
        firstName = node.get("firstName").asText();
    }
    if(node.has("lastName") && !node.get("lastName").asText().isEmpty()) {
        lastName = node.get("lastName").asText();
    }
    if(node.has("age") && !node.get("age").asText().isEmpty()) {
        age = node.get("age").asText();
    }
    if(node.has("address") && !node.get("address").asText().isEmpty()) {
        address = node.get("address").asText();
    }
    if(firstName == null && lastName == null && age == null && address == null) {
        return null;
    }
    return new User(firstName, lastName, age, address);
}

}

现在,这可用于反序列化整个 User 对象:

示例输入:

{
    "firstName" : "",
    "lastName" : "Paul",
    "age" : "31"
}

会反序列化成:

User{firstName='null', lastName='Paul', age='31', address='null'}

【讨论】:

  • 我正在测试您的解决方案,但我有一个带有 LocalDate 而不是字符串的案例,如何更改 if 以使其也适用于该类型?
  • 您需要将LocalDate字段读取为文本,然后将其转换为LocalDate。假设该字段是LocalDate dateOfBirth。那么CustomDeserializer 中的 if 条件将是 if(node.has("dateOfBirth") &amp;&amp; !node.get("dateOfBirth").asText().isEmpty()) { dateOfBirth = LocalDate.parse(node.get("dateOfBirth").asText()); }
  • 我尝试使用您的解决方案,但它一直给我同样的问题。如果我在类上插入注释,它会抛出这个错误“MethodArgumentNotValidException”,而在单个字段上它会用 null 覆盖每个值。解决方案中是否有技巧可做?
  • 您能否分享堆栈跟踪信息,以便提供有关错误发生位置的更多详细信息?
  • 我检查并注意到我的错误,您的代码是正确的。只是想问一下,如果这对你来说是个问题,我会创建另一个问题,如果你可以优化这个答案,即是否可以不为每个变量实例化 null?所以我避免每次发生变化时我也必须更改反序列化器
猜你喜欢
  • 2020-06-28
  • 1970-01-01
  • 1970-01-01
  • 2018-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多