如何忽略像“car”这样的空 json 对象:,这会在使用 jackson 反序列化后导致空 pojos

Posted

技术标签:

【中文标题】如何忽略像“car”这样的空 json 对象:,这会在使用 jackson 反序列化后导致空 pojos【英文标题】:How ignore empty json objects like "car":, that cause empty pojos after deserialisation with jackson如何忽略像“car”这样的空 json 对象:,这会在使用 jackson 反序列化后导致空 pojos 【发布时间】:2021-03-15 17:38:22 【问题描述】:

我有一个休息 service 消耗来自 Angular UI 和其他休息客户端的 json。数据基于约 50 个实体的复杂结构,这些实体存储在具有约 50 个表的数据库中。问题在于可选的 OneToOne 关系,因为 Angular 将可选对象作为空定义发送,例如 "car": ,。 spring 数据存储库将它们保存为空条目,我得到了一个 Json 响应,如"car": "id": 545234, "version": 0。我发现没有 Jackson 注释可以忽略空对象,只有空或 null 属性。

员工实体具有以下形式:

@Entity
public class Employee 
  @Id 
  @GeneratedValue
  private Long id;
  
  @Version
  private Long version;

  private String name;

  @OneToOne(cascade = CascadeType.ALL)
  @JoinColumn(name = "car_id")
  @JsonManagedReference
  private Car car;

  .
  .   Desk, getters and setters
  . 

和 OneToOne 参考的另一面

@Entity
public class Car
  @Id
  @GeneratedValue
  private Long id;

  @Version
  private Long version;

  private String name;

  @OneToOne(fetch = FetchType.LAZY, mappedBy = "employee")
  @JsonBackReference
  private Employee employee;


  .
  .   getters and setters
  . 

例如,我将此作为后期操作发送到我的服务


  "name": "ACME",
      .
      .
      .
  "employee": 
    "name": "worker 1",
    "car": ,
    "desk": 
      floor: 3,
      number: 4,
      phone: 444
    
      .
      .
      .
  ,
  "addresses": [],
  "building": ,
      .
      .
      .

我得到了保存的数据作为响应


  "id": 34534,
  "version": 0,
  "name": "ACME",
      .
      .
      .
  "employee": 
    "id": 34535,
    "version":0,
    "name": "worker 1",
    "car": "id": 34536, "version": 0,
    "desk": 
      "id": 34538,
      "version":0,
      "floor": 3,
      "number": 4,
      "phone": 444
    
      .
      .
      .
  ,
  "addresses": [],
  "building": "id": 34539, "version": 0,
      .
      .
      .

正如在响应中看到的,我得到了带有 id、版本、许多空值和空字符串的空表行,因为当我保存(持久)主要的反序列化公司类时,其他实体也被保存,因为是注释为级联。

我发现了很多像 Do not include empty object to Jackson 这样的例子,有一个具体的 pojo 和一个具体的反序列化器正在工作,但每个实体都需要自己的反序列化器。这会为当前实体和未来的新实体(仅可选实体)带来许多工作。

我尝试了以下方法,我写了一个 BeanDeserializerModifier 并尝试在标准的 beandeserializer 上包装一个自己的反序列化器:

    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier() 
        @Override
        public List<BeanPropertyDefinition> updateProperties(DeserializationConfig config,
                                                             BeanDescription beanDesc,
                                                             List<BeanPropertyDefinition> propDefs) 
            logger.debug("update properties, beandesc: ", beanDesc.getBeanClass().getSimpleName());
            return super.updateProperties(config, beanDesc, propDefs);
        

        @Override
        public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
                                                      BeanDescription beanDesc,
                                                      JsonDeserializer<?> deserializer) 
            
            logger.debug("modify deserializer ",beanDesc.getBeanClass().getSimpleName());
            // This fails:
           // return new DeserializationWrapper(deserializer, beanDesc);
            return deserializer; // This works, but it is the standard behavior
        
    );

这是包装器(和错误):

public class DeserializationWrapper extends JsonDeserializer<Object> 
private static final Logger logger = LoggerFactory.getLogger( DeserializationWrapper.class );

    private final JsonDeserializer<?> deserializer;
    private final BeanDescription beanDesc;

    public DeserializationWrapper(JsonDeserializer<?> deserializer, BeanDescription beanDesc) 
        this.deserializer = deserializer;
        this.beanDesc = beanDesc;
    

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException 
        logger.debug("deserialize in wrapper  ",beanDesc.getBeanClass().getSimpleName());
        final Object deserialized = deserializer.deserialize(p, ctxt);
        ObjectCodec codec = p.getCodec();
        JsonNode node = codec.readTree(p);
        
         // some logig that not work
         // here. The Idea is to detect with the json parser that the node is empty.
         // If it is empty I will return null here and not the deserialized pojo

        return deserialized;
    

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException 
        logger.debug("deserializer - method 2");
        intoValue = deserializer.deserialize(p, ctxt);
        return intoValue;
    

    @Override
    public boolean isCachable() 
        return deserializer.isCachable();
    
  .
  .     I try to wrap the calls to the deserializer
  .

反序列化包装器在第一次调用后不起作用并崩溃,异常com.fasterxml.jackson.databind.exc.MismatchedInputException: No _valueDeserializer assigned at [Source: (PushbackInputStream); line: 2, column: 11] (through reference chain: ... Company["name"])

我的问题:有没有办法扩展工作标准解串器的行为,解串器在解析时检测到当前 jsonNode 为空并返回 null 而不是空类实例?也许我的想法是错误的,还有完全其他的解决方案?

在 Angular UI 方面解决它是没有选择的。我们使用 Jackson 2.9.5。

【问题讨论】:

您使用的是 spring-boot 休息控制器吗?为什么需要客户解串器?您能否分享一个演示 GIT 存储库以便更好地理解? 我写了一个POC并推送到github.com/joe-bookwood/empty-objects-poc 【参考方案1】:

BeanDeserializerModifier 与自定义反序列化器一起使用是个好主意。您只需要改进反序列化器的实现。在您的示例中,问题出在以下几行:

final Object deserialized = deserializer.deserialize(p, ctxt); //1.
ObjectCodec codec = p.getCodec(); //2.
JsonNode node = codec.readTree(p); //3.

1. 行读取 JSON Object。在2.3. 行中,您想创建一个JsonNode,但1. 中已经读取了空的JSON Object。接下来的两行将尝试将剩余的有效负载读取为JsonNode

Jackson 默认使用BeanDeserializer 反序列化常规POJO 类。我们可以扩展这个类并提供我们自己的实现。在版本 2.10.1 deserialise 中,方法如下所示:

@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException

    // common case first
    if (p.isExpectedStartObjectToken()) 
        if (_vanillaProcessing) 
            return vanillaDeserialize(p, ctxt, p.nextToken());
        
        // 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
        //    what it is, including "expected behavior".
        p.nextToken();
        if (_objectIdReader != null) 
            return deserializeWithObjectId(p, ctxt);
        
        return deserializeFromObject(p, ctxt);
    
    return _deserializeOther(p, ctxt, p.getCurrentToken());

在大多数情况下,在不需要特殊处理的情况下,vanillaDeserialize 方法将被调用。一起来看看吧:

private final Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException 
    final Object bean = _valueInstantiator.createUsingDefault(ctxt);
    // [databind#631]: Assign current value, to be accessible by custom serializers
    p.setCurrentValue(bean);
    if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) 
        String propName = p.getCurrentName();
        do 
            p.nextToken();
            SettableBeanProperty prop = _beanProperties.find(propName);

            if (prop != null)  // normal case
                try 
                    prop.deserializeAndSet(p, ctxt, bean);
                 catch (Exception e) 
                    wrapAndThrow(e, bean, propName, ctxt);
                
                continue;
            
            handleUnknownVanilla(p, ctxt, bean, propName);
         while ((propName = p.nextFieldName()) != null);
    
    return bean;

如您所见,它几乎可以满足我们的所有需求,除了它会为空的JSON Object 创建新对象。它在创建新对象后检查字段是否存在。一条线太远了。不幸的是,这个方法是私有的,我们不能覆盖它。让我们把它复制到我们的类中并稍微修改一下:

class EmptyObjectIsNullBeanDeserializer extends BeanDeserializer 

    EmptyObjectIsNullBeanDeserializer(BeanDeserializerBase src) 
        super(src);
    

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException 
        if (_vanillaProcessing) 
            return vanillaDeserialize(p, ctxt);
        

        return super.deserialize(p, ctxt);
    

    private Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt) throws IOException 
        p.nextToken();
        if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) 
            final Object bean = _valueInstantiator.createUsingDefault(ctxt);
            // [databind#631]: Assign current value, to be accessible by custom serializers
            p.setCurrentValue(bean);
            String propName = p.getCurrentName();
            do 
                p.nextToken();
                SettableBeanProperty prop = _beanProperties.find(propName);

                if (prop != null)  // normal case
                    try 
                        prop.deserializeAndSet(p, ctxt, bean);
                     catch (Exception e) 
                        wrapAndThrow(e, bean, propName, ctxt);
                    
                    continue;
                
                handleUnknownVanilla(p, ctxt, bean, propName);
             while ((propName = p.nextFieldName()) != null);
            return bean;
        
        return null;
    

你可以像下面这样注册:

class EmptyObjectIsNullBeanDeserializerModifier extends BeanDeserializerModifier 
    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) 
        if (beanDesc.getBeanClass() == Car.class)  //TODO: change this condition
            return new EmptyObjectIsNullBeanDeserializer((BeanDeserializerBase) deserializer);
        
        return super.modifyDeserializer(config, beanDesc, deserializer);
    

简单POC:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Data;

import java.io.File;
import java.io.IOException;

public class EmptyObjectIsNullApp 

    public static void main(String[] args) throws IOException 
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        SimpleModule emptyObjectIsNullModule = new SimpleModule();
        emptyObjectIsNullModule.setDeserializerModifier(new EmptyObjectIsNullBeanDeserializerModifier());

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(emptyObjectIsNullModule);

        Wrapper wrapper = mapper.readValue(jsonFile, Wrapper.class);
        System.out.println(wrapper);
    


@Data
class Wrapper 
    private Car car;


@Data
class Car 
    private int id;

对于“空对象”JSON 有效载荷:


  "car": 

上面的代码打印:

Wrapper(car=null)

对于带有某些字段的JSON 有效负载:


  "car": 
    "id": 1
  

上面的代码打印:

Wrapper(car=Car(id=1))

【讨论】:

Beandeserializer 的扩展是神奇的。太好了 - 没有错误了。我为我的实体类编写了一个 isEmpty 接口。所以我可以在每个实体中定义什么是空的,并且所有 OnToOne 实体都以这种方式标记。当我准备好时,我会在这里通知你我的结果。谢谢!! "isEmpty interface" 与OOP 非常一致。这也是一个好主意,在这种情况下可能非常可靠。调整像 Jackson 这样的库有时真的很难,应该小心。 在我的例子中,vanillaDeserialize 从未调用过,__vanillaProcessing 始终是false,在原始BeanDeserializer 中。在我的例子中,BeanDeserializer 跳转到deserializeFromObject。我现在开始分析它。我也踢掉了 isEmpty 方法,它很复杂。我认为它更容易依赖于属性(子)的零计数。 我添加了具有双向 OneToOne 关系的 EmployeeCar 实体。我在双向关系中使用@JsonManagedReference@JsonBackreference。这是防止无限循环所必需的,但是当我考虑时,这可能是杰克逊使用deserializeFromObject的原因。 @JochenBuchholz,看起来我们需要重写更多方法:包括deserializeFromObject。我们可以检查与香草方法相同的条件。将此添加到EmptyObjectIsNullBeanDeserializer 类:public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) return super.deserializeFromObject(p, ctxt);return null;

以上是关于如何忽略像“car”这样的空 json 对象:,这会在使用 jackson 反序列化后导致空 pojos的主要内容,如果未能解决你的问题,请参考以下文章

如何忽略 JSON 解析回数据中的某些对象?

RestKit 对象映射在 BOOL 的空 JSON 值上崩溃

忽略急切加载的数据的空对象 - laravel

如何忽略 DataWeave Mule esb 中的空对象

如何读取嵌套的 json 对象? [复制]

如何使用 restkit 忽略已发布对象上的空属性