Summary
JSONReader.Feature.IgnoreSetNullValue is documented to skip setter calls when the incoming JSON value is null. It works for fields deserialized via FieldReaderObject, but is silently ignored for fields deserialized via the specialized subclasses — most commonly FieldReaderString, which is picked for every String field on a bean.
This means neither of the documented ways to enable the feature actually works for String fields:
JSON.parseObject(json, Clazz.class, JSONReader.Feature.IgnoreSetNullValue) — parse-time context feature.
@JSONType(deserializeFeatures = JSONReader.Feature.IgnoreSetNullValue) on the bean — annotation-level feature that correctly propagates into FieldReader.features.
I verified both against the current master (2.0.62-SNAPSHOT, commit 3697c2d37) and against 2.0.53.
Reproducer
public class Repro {
@JSONType(deserializeFeatures = JSONReader.Feature.IgnoreSetNullValue)
public static class Sample {
private String name = "Default";
public String getName() { return name; }
public void setName(String n) { this.name = n; }
}
public static void main(String[] args) {
Sample s = JSON.parseObject("{\"name\":null}", Sample.class);
System.out.println(s.getName()); // expected "Default", actual null
}
}
Also fails with the context-feature form:
JSON.parseObject("{\"name\":null}", Sample.class, JSONReader.Feature.IgnoreSetNullValue);
// expected name == "Default", actual null
Root cause
Debug-inspecting the generated reader confirms the feature does flow correctly into the FieldReader when @JSONType(deserializeFeatures=...) is used:
Reader class: com.alibaba.fastjson2.reader.ORG_1_1_Sample
FieldReader class: com.alibaba.fastjson2.reader.FieldReaderString
IgnoreSetNullValue in FieldReader.features? true <-- correctly propagated
Parsed name = null <-- but ignored at parse time
The propagation chain works:
ObjectReaderBaseModule.getBeanInfo reads @JSONType.deserializeFeatures and ORs them into beanInfo.readerFeatures.
ObjectReaderCreator ORs beanInfo.readerFeatures into fieldInfo.features.
- The resulting
FieldReaderString.features contains the IgnoreSetNullValue mask.
The feature is then ignored at the FieldReader subclass level. FieldReaderObject.accept(T, Object) correctly gates on it:
// FieldReaderObject.java:300
if (isParameter() || (value == null && (features & JSONReader.Feature.IgnoreSetNullValue.mask) != 0)) {
return;
}
But FieldReaderString.accept(T, Object) and FieldReaderString.readFieldValue(JSONReader, T) do not check the feature — they call propertyAccessor.setObject(object, fieldValue) regardless of fieldValue == null:
// FieldReaderString.java:52-83 — no IgnoreSetNullValue guard
@Override
public void accept(T object, Object value) {
String fieldValue = ...;
...
propertyAccessor.setObject(object, fieldValue);
}
// FieldReaderString.java:87- — no IgnoreSetNullValue guard
@Override
public void readFieldValue(JSONReader jsonReader, T object) {
String fieldValue = jsonReader.readString();
...
propertyAccessor.setObject(object, fieldValue);
}
A grep -n IgnoreSetNullValue core/src/main/java/.../reader/ shows the feature is honored in only a handful of readers: FieldReaderObject, FieldReaderZonedDateTime, FieldReaderLocalDateTime, FieldReaderInstant, FieldReaderStackTrace, plus ObjectReaderAdapter#createInstance(Map, features). All other specialized FieldReaders silently ignore it, including FieldReaderString and the primitive-boxed variants.
Suggested fix
Add the same null-skip gate used in FieldReaderObject to every specialized FieldReader subclass's accept(T, Object) and readFieldValue(JSONReader, T) paths. Minimal per-class diff, e.g. for FieldReaderString:
if (fieldValue == null && (features & JSONReader.Feature.IgnoreSetNullValue.mask) != 0) {
return;
}
Environment
- fastjson2: 2.0.53 and master (
2.0.62-SNAPSHOT, commit 3697c2d37)
- JDK: 21
- OS: macOS
Context
Encountered while migrating a Spring FastJsonHttpMessageConverter-based service from Jackson (@JsonSetter(nulls = Nulls.SKIP)) to fastjson2. No combination of FastJsonConfig.setReaderFeatures, module-level BeanInfo.readerFeatures injection, or @JSONType(deserializeFeatures=...) restored Nulls.SKIP parity for String fields. Tracing confirmed the feature reaches the FieldReader correctly — it is simply not consulted by FieldReaderString.
Summary
JSONReader.Feature.IgnoreSetNullValueis documented to skip setter calls when the incoming JSON value isnull. It works for fields deserialized viaFieldReaderObject, but is silently ignored for fields deserialized via the specialized subclasses — most commonlyFieldReaderString, which is picked for everyStringfield on a bean.This means neither of the documented ways to enable the feature actually works for
Stringfields:JSON.parseObject(json, Clazz.class, JSONReader.Feature.IgnoreSetNullValue)— parse-time context feature.@JSONType(deserializeFeatures = JSONReader.Feature.IgnoreSetNullValue)on the bean — annotation-level feature that correctly propagates intoFieldReader.features.I verified both against the current master (
2.0.62-SNAPSHOT, commit3697c2d37) and against2.0.53.Reproducer
Also fails with the context-feature form:
Root cause
Debug-inspecting the generated reader confirms the feature does flow correctly into the FieldReader when
@JSONType(deserializeFeatures=...)is used:The propagation chain works:
ObjectReaderBaseModule.getBeanInforeads@JSONType.deserializeFeaturesand ORs them intobeanInfo.readerFeatures.ObjectReaderCreatorORsbeanInfo.readerFeaturesintofieldInfo.features.FieldReaderString.featurescontains theIgnoreSetNullValuemask.The feature is then ignored at the FieldReader subclass level.
FieldReaderObject.accept(T, Object)correctly gates on it:But
FieldReaderString.accept(T, Object)andFieldReaderString.readFieldValue(JSONReader, T)do not check the feature — they callpropertyAccessor.setObject(object, fieldValue)regardless offieldValue == null:A
grep -n IgnoreSetNullValue core/src/main/java/.../reader/shows the feature is honored in only a handful of readers:FieldReaderObject,FieldReaderZonedDateTime,FieldReaderLocalDateTime,FieldReaderInstant,FieldReaderStackTrace, plusObjectReaderAdapter#createInstance(Map, features). All other specialized FieldReaders silently ignore it, includingFieldReaderStringand the primitive-boxed variants.Suggested fix
Add the same null-skip gate used in
FieldReaderObjectto every specialized FieldReader subclass'saccept(T, Object)andreadFieldValue(JSONReader, T)paths. Minimal per-class diff, e.g. forFieldReaderString:Environment
2.0.62-SNAPSHOT, commit3697c2d37)Context
Encountered while migrating a Spring
FastJsonHttpMessageConverter-based service from Jackson (@JsonSetter(nulls = Nulls.SKIP)) to fastjson2. No combination ofFastJsonConfig.setReaderFeatures, module-levelBeanInfo.readerFeaturesinjection, or@JSONType(deserializeFeatures=...)restoredNulls.SKIPparity forStringfields. Tracing confirmed the feature reaches the FieldReader correctly — it is simply not consulted byFieldReaderString.