alibaba / alibaba/fastjson2

IgnoreSetNullValue ignored by specialized FieldReader subclasses (FieldReaderString et al.)

Open
#7,618 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
4.4k
Forks
613
Avg merge
1d 22h
Merged PRs (30d)
6

Description

## 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:

1. `JSON.parseObject(json, Clazz.class, JSONReader.Feature.IgnoreSetNullValue)` — parse-time context feature.
2. `@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

```java
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:

```java
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:

```java
// 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`:

```java
// 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`:

```java
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`.

Contributor guide

Open the contributing guide

Research direction

Start with FieldReaderString.java and compare its accept and readFieldValue paths with the existing null-skip guard in FieldReaderObject.java. Review the other specialized FieldReader subclasses identified by the issue, then run the provided Sample reproducer. Done means IgnoreSetNullValue preserves the existing String value instead of assigning null through each affected path.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.