本文罗列了笔者遇到的JSON使用中不得不注意的feature和坑。
就本次【粗糙的】
兼容性试验来看,Jackson > Gson > fastjson
易用性试验来看,Gson = fastjson > Jackson(Jackson的filter,真jr难用)
也可能在之后使用中出现打脸情况。也欢迎读者评论区打脸。
fastjson版本:1.2.58(特别标明,因为fastjson在1.2.29与1.2.58表现不同。1.2.51以下有安全漏洞)
gson版本:2.3.1
jackson-bind版本:2.9.3
场景1:JSON规范,互相parse
就Map<Long, String>的序列化方法,fastjson和另两位想法不同。
源类定义:
private Integer a;
private String b;
private List<Short> cList;
private Map<Long, String> dMap;
private Boolean e;
private Object f;
目标类定义:
private Integer a;
private String b;
private List<Short> cList;
private Map<Long, String> dMap;
private Object f;
序列化字符串:
gson: {"a":1,"b":"bbbbb","cList":[3],"dMap":{"4":"ddd"},"f":6}
fastjson: {"a":1,"b":"bbbbb","cList":[3],"dMap":{4:"ddd"},"f":6}
jackson: {"a":1,"b":"bbbbb","e":null,"f":6,"clist":[3],"dmap":{"4":"ddd"}}
在dmap这个字段上,所有版本的fastjson默认配置下,特立独行,并没有给map的key带上引号。需要手动指定SerializerFeature.WriteNonStringKeyAsString。
// 个人认为这是fastjson的最佳实践
static {
JSON.DEFAULT_GENERATE_FEATURE |= SerializerFeature.WriteNonStringKeyAsString.getMask();
}
若不指定SerializerFeature.WriteNonStringKeyAsString,会导致Gson, Jackson无法识别fastjson序列化生成的json串,兼容性极差。
由于此问题,如果fastjson不指定SerializerFeature.WriteNonStringKeyAsString,在进行3x3的9次互相parse中,gson和jackson都表示不认识fastjson序列化的字符串,抛出了错误。
此外,对null的字段e,只有jackson一板一眼地将null值写了出来。更改配置可以让Jackson忽略null值的序列化:
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
场景2:fastjson的历史兼容问题
给定String:{"dMap":{1:"1.0"}} (注意,这个string是fastjson生成的。它本身在jackson和gson眼里就是非法的。)
fastjson在1.2.29和1.2.58中,对JSON.parseObject(obj, Map.class)的返回表现不同。
1.2.29中,反序列化得到的dMap,key是String。这与jackson兼容。
1.2.58中,反序列化得到的dMap,key是Integer。将此map通过Spring Controller返回时,提交给jackson序列化,会报错。
这个问题,也可以通过场景1中的SerializerFeature.WriteNonStringKeyAsString绕过。因为有了这个选项,就不会生成出现此场景给定的迷之非法“json”字符串。当然,如果串是别人传入的,就只能自己注意此差异了。
场景3:对数字的认知差异
f字段是一个原始类对象。对此对象赋值为1(int),在反序列化之后,jackson和fastjson都认定为Integer,但gson会认定为Double。
没有声明类所以本来没什么优劣,但实际使用中,Double.toString会自动补一个'.0'在末尾,可能造成问题(见场景4)。
场景4:parse为map
使用Map.class接反序列化结果时,jackson能保持字面原样,内层的dMap识别为Map<String, String>。
fastjson内层的dMap识别成自定义的Map的继承类JSONObject。
gson表现很迷,会将原Integer转换成Double,且作者表示JSON中数字是没有区别的,所以坚持在Gson中不做区别:https://github.com/google/gson/issues/1084。解决方案见 java - How can I prevent gson from converting integers to doubles - Stack Overflow,关键字:MapDeserializerDoubleAsIntFix。
场景5:源对象比目标对象字段多
由于源对象比目标对象多一个字段e,jackson的默认配置下会抛出UnrecognizedPropertyException。特例:如果多出的字段由于(为null且被Gson序列化)或(其他原因),序列化为string没有表现出来时,jackson会逃过一劫,正常反序列化。
通过配置jackson的objectMapper,禁用反序列化选项FAIL_ON_UNKNOWN_PROPERTIES,可以解决此问题:
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
场景6:字符串中的小数,反序列化为整数
当字符串中为小数,反序列化为整数时,不同框架表现不同。
jackson:自动转为整数。
Gson:抛出NumberFormatException。
fastjson:不同版本表现不同。1.2.29自动转(BigDecimal.intValue),1.2.29.sec04如果有小数部分,抛出ArithmeticException,1.2.58对小数位数<100的自动转。
场景7:HttpServletRequest的序列化
HttpServletRequest是Spring中声明Controller并对请求做若干操作时的常用类。但此类的对象,使用3种不同的序列化工具都无法正常序列化。
fastjson抛出JSONException, Caused by: java.lang.IllegalStateException: s=DISPATCHED i=true a=NOT_ASYNC
jackson与gson抛出stackOverflow(多半是由于对象中存在环形引用)。
场景8:序列化规则指定
业务场景是:对字段中,以"set"打头和"Iterator"结尾的属性,不做序列化。
fastjson: JSON.toJSONString中,指定SerializeFilter
private static PropertyFilter getPropertyFilter(String ignore) {
return (source, name, value) -> !name.equals(ignore) && !name.startsWith("set") && value != null && !name.endsWith("Iterator");
}
private static PropertyFilter getPropertyFilter() {
return getPropertyFilter(StringUtils.EMPTY);
}
public static String toJSONString(Object src) {
return JSON.toJSONString(obj, getPropertyFilter());
}
Gson: 生成Gson对象时,指定SerializeStrategy。见 Exclude Fields from Serialization in Gson | Baeldung
Jackson: 必须使用注解在源类上!对源类没操作权限时,需要自定义MixIn类,并在Mapper里指定类名映射!然后自行实现SimpleBeanPropertyFilter!绕过手段:jackson序列化,指定属性筛选_jackson filterprovider-优快云博客
场景9:非静态内部类
由于java的机制,非静态内部类会持有外部类的实体(以获得外部类的属性)。在对应的字节码实现中,此类的构造函数依然有一个带有父类对象引用的参数,所以此类无法拥有无参构造(即便我们显式声明了无参构造)。
对这种java类,fastJSON做了一些兼容,能支持非静态内部类的反序列化。而Jackson反序列化会报错。
附:Jackson常见序列化/反序列化配置
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);