Fastjson序列化空Map出现key为"$ref"的键值对的问题

Fastjson序列化空Map出现key为"$ref"的键值对的问题

LonelyMan 19 2024-04-09

起因

业务中创建了一个存储表格数据的对象,如下

@Data
public class SheetDataDto {

    private List<SheetColumnInfo> columns;
    private List<SheetRowInfo> rows;
    private Map<String, Object> meta;

    @Data
    public static class SheetColumnInfo {
        private String field;
        private String title;
        private String typeName;
        private Map<String, Object> meta;

        public SheetColumnInfo() {
            this.meta = MapUtil.empty();
        }
    }

    @Data
    public static class SheetRowInfo {
        private List<CellInfo> cells;
        private Integer createId;
        private Map<String, Object> meta;

        public SheetRowInfo() {
            this.cells = new ArrayList<>();
            this.meta = MapUtil.empty();
        }

        public SheetRowInfo(Integer createId) {
            this.createId = createId;
            this.cells = new ArrayList<>();
            this.meta = MapUtil.empty();
        }
    }

    @Data
    public static class CellInfo {
        private String field;
        private String value;
        private Map<String, Object> meta;

        public CellInfo() {
            this.meta = MapUtil.empty();
        }
    }

    public SheetDataDto() {
        this.columns = new ArrayList<>();
        this.rows = new ArrayList<>();
        this.meta = MapUtil.empty();
    }
}

该类包含嵌套结构,并且由于各个meta属性在构造时需要为空,所以用了MapUtil.empty()直接赋值给了meta

构造一个新对象,并对属性赋值,对象内容如下

SheetDataDto(columns=[SheetDataDto.SheetColumnInfo(field=jsm2dr, title=Name, typeName=string, meta={}), SheetDataDto.SheetColumnInfo(field=gadh9i, title=Notes, typeName=string, meta={}), SheetDataDto.SheetColumnInfo(field=xq1n1n, title=Status, typeName=string, meta={})], rows=[SheetDataDto.SheetRowInfo(cells=[], createId=1, meta={}), SheetDataDto.SheetRowInfo(cells=[], createId=1, meta={}), SheetDataDto.SheetRowInfo(cells=[], createId=1, meta={})], meta={})

但是在使用Fastjson对该对象进行序列化时,各个meta会被序列化为 "meta":{"$ref":"$.data.columns[0].meta"},可是这个meta本身就是一个空Map

{
  "meta": { "$ref": "$.columns[0].meta" },
  "rows": [
    { "meta": { "$ref": "$.columns[0].meta" }, "cells": [], "createId": 1 },
    { "meta": { "$ref": "$.columns[0].meta" }, "cells": [], "createId": 1 },
    { "meta": { "$ref": "$.columns[0].meta" }, "cells": [], "createId": 1 }
  ],
  "columns": [
    {
      "meta": {},
      "field": "jsm2dr",
      "title": "Name",
      "typeName": "string"
    },
    {
      "meta": { "$ref": "$.columns[0].meta" },
      "field": "gadh9i",
      "title": "Notes",
      "typeName": "string"
    },
    {
      "meta": { "$ref": "$.columns[0].meta" },
      "field": "xq1n1n",
      "title": "Status",
      "typeName": "string"
    }
  ]
}

分析

Fastjson 在序列化过程中检测到循环引用(即对象结构中存在相互引用),为了防止无限递归和栈溢出,Fastjson 会自动启用循环引用检测,并使用$ref来引用已经序列化过的相同对象,避免重复输出

在上述问题中,SheetDataDtoSheetColumnInfoSheetRowInfo对象内部的 meta 属性虽然都为空,但MapUtil.empty()是一个静态的空Map,三个对象的meta其实都是同一个对象。当 Fastjson 序列化这些对象时,由于它们的 meta 属性都为空且互相引用,为了避免重复输出空对象,Fastjson 选择了使用 $ref 引用来替代直接输出空 meta 对象

解决

1、消除循环引用
this.meta = MapUtil.empty();替换为this.meta = new HashMap<>();,让所有的meta都是互不相关的空Map

2、设置 SerializerFeature
使用 SerializerFeature.DisableCircularReferenceDetect 特性可以禁用 Fastjson 的循环引用检测功能

JSON.toJSONString(sheet, SerializerFeature.DisableCircularReferenceDetect);

警告: 禁用循环引用检测可能会导致无限递归或栈溢出问题,如果对象结构中确实存在循环引用,请谨慎使用此方法

3、使用 JSON.toJSONStringZ()
Fastjson 提供了一个 toJSONStringZ() 方法,它会在序列化时忽略空对象

JSON.toJSONStringZ(sheet);

注意: toJSONStringZ() 方法可能会改变原始序列化的结果,因为它会忽略所有空对象,而不仅仅是引起循环引用的空对象,并且该方法已被标记为deprecated

参考