Why a Single Log Line Triggered a FastJSON NullPointerException – Deep Debugging Guide
The article walks through a real‑world FastJSON serialization failure caused by a newly added log statement, explains why FastJSON invokes certain getter methods during serialization, shows the ASM‑generated serializer internals, and proposes annotation‑based coding standards to avoid similar bugs.
Incident Review
A simple log line was added to the review code before a nightly deployment. Although the change seemed harmless, the service immediately generated numerous alerts, prompting an urgent rollback and removal of the logging code.
Scenario Reconstruction
Define a CountryDTO class:
public class CountryDTO {
private String country;
public void setCountry(String country) {
this.country = country;
}
public String getCountry() {
return this.country;
}
public Boolean isChinaName() {
return this.country.equals("中国");
}
}Define a test class FastJonTest that serializes the DTO:
public class FastJonTest {
@Test
public void testSerialize() {
CountryDTO countryDTO = new CountryDTO();
String str = JSON.toJSONString(countryDTO);
System.out.println(str);
}
}Running the test throws a NullPointerException because FastJSON calls isChinaName() during serialization while the country field is still null.
Why does serialization invoke isChinaName()?
What other methods are executed during the serialization process?
Source Code Analysis
Debugging the call stack reveals that FastJSON generates a class named ASMSerializer_1_CountryDTO using the ASM library. The generated class’s write method is invoked by FastJSON’s serializer.
ASM is used to create a serializer class at runtime, replacing Java reflection and thus avoiding the overhead of repeated reflective calls.
JavaBeanSerializer Serialization Principle
During serialization FastJSON ultimately calls JavaBeanSerializer.write. This method obtains an ObjectWriter via getObjectWriter(), which leads to the creation of a JavaBeanSerializer inside SerializeConfig#createJavaBeanSerializer. The critical method in this chain is com.alibaba.fastjson.util.TypeUtils#computeGetters, which determines which getters are included.
public static List<FieldInfo> computeGetters(Class<?> clazz, //
JSONType jsonType, //
Map<String,String> aliasMap, //
Map<String,Field> fieldCacheMap, //
boolean sorted, //
PropertyNamingStrategy propertyNamingStrategy //
) {
// ... omitted code ...
Method[] methods = clazz.getMethods();
for (Method method : methods) {
// ... omitted code ...
if (method.getReturnType().equals(Void.TYPE)) {
continue;
}
if (method.getParameterTypes().length != 0) {
continue;
}
JSONField annotation = TypeUtils.getAnnotation(method, JSONField.class);
if (annotation != null) {
if (!annotation.serialize()) {
continue;
}
if (annotation.name().length() != 0) {
// ... omitted code ...
}
}
if (methodName.startsWith("get")) {
// ... omitted code ...
}
if (methodName.startsWith("is")) {
// ... omitted code ...
}
}
}The analysis shows three categories of methods that FastJSON may invoke: @JSONField(serialize = false, name = "xxx") annotated methods are excluded.
Methods whose names start with get (standard getters).
Methods whose names start with is (boolean‑style getters).
Code Standards
To make the serialization intent explicit and avoid accidental method execution, the article recommends using @JSONField(serialize = false) on methods that should not participate in serialization.
public class CountryDTO {
private String country;
public void setCountry(String country) {
this.country = country;
}
public String getCountry() {
return this.country;
}
@JSONField(serialize = false)
public static void queryCountryList() {
System.out.println("queryCountryList() executed!!");
}
public Boolean isChinaName() {
System.out.println("isChinaName() executed!!");
return true;
}
public String getEnglishName() {
System.out.println("getEnglishName() executed!!");
return "lucy";
}
@JSONField(serialize = false)
public String getOtherName() {
System.out.println("getOtherName() executed!!");
return "lucy";
}
@JSONField(serialize = false)
public String getEnglishName2() {
System.out.println("getEnglishName2() executed!!");
return "lucy";
}
@JSONField(serialize = false)
public void getEnglishName3() {
System.out.println("getEnglishName3() executed!!");
}
@JSONField(serialize = false)
public String isChinaName2() {
System.out.println("isChinaName2() executed!!");
return "isChinaName2";
}
}Common Serialization Scenarios
The following diagram summarizes the typical flow of a FastJSON serialization issue, from problem discovery to principle analysis, solution implementation, and finally coding standards.
In summary, the root cause was that FastJSON, via its ASM‑generated serializer, automatically called the isChinaName() getter during serialization. By explicitly marking non‑serializable methods with @JSONField(serialize = false), developers can prevent similar null‑pointer crashes and make the serialization contract clear.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
