Why a Single Log Line Triggered a FastJSON NullPointer – A Deep Dive into Java Serialization

The article recounts a production incident where adding a trivial log statement caused a FastJSON NullPointerException, then explains the underlying serialization mechanism, the role of ASM-generated serializers, the methods invoked during serialization, and provides best‑practice guidelines to avoid similar bugs.

macrozheng
macrozheng
macrozheng
Why a Single Log Line Triggered a FastJSON NullPointer – A Deep Dive into Java Serialization

Incident Review

During a recent deployment a developer added a one‑line log statement while reviewing code, assuming it would be harmless. After the release, a flood of alerts appeared, prompting an immediate rollback. The root cause was a NullPointerException triggered during JSON serialization.

Scenario Reconstruction

A CountryDTO class was defined with a String country field, standard getters/setters, and an isChinaName() method that checks this.country.equals("中国"). A test class FastJonTest serializes an instance of CountryDTO using FastJSON:

public class FastJonTest {
    @Test
    public void testSerialize() {
        CountryDTO countryDTO = new CountryDTO();
        String str = JSON.toJSONString(countryDTO);
        System.out.println(str);
    }
}

The execution throws a NullPointerException because isChinaName() is invoked while country is still null.

Key Questions

Why does the serialization process call isChinaName()?

What other methods are executed during FastJSON serialization?

Source Code Analysis

Debugging reveals the call chain ending at ASMSerializer_1_CountryDTO.write, a class dynamically generated by FastJSON using ASM bytecode manipulation. ASM is employed to create serializer classes at runtime, reducing the overhead of Java reflection.

ASM can dynamically generate classes to replace reflective calls, thereby improving performance.

JavaBeanSerializer Serialization Principle

FastJSON ultimately delegates to JavaBeanSerializer.write(). The serializer obtains an ObjectWriter via getObjectWriter(), which relies on SerializeConfig#createJavaBeanSerializer and TypeUtils#computeGetters. The computeGetters method scans class methods and selects those that should participate in serialization.

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) {
        if (method.getReturnType().equals(Void.TYPE)) continue;
        if (method.getParameterTypes().length != 0) continue;
        // process @JSONField annotation, getter naming conventions, etc.
    }
}

The method classifies eligible getters into three categories: @JSONField(serialize = false, name = "xxx") annotations explicitly exclude a method.

Methods starting with get (e.g., getXxx()) are considered getters.

Methods starting with is (e.g., isXxx()) are considered boolean getters.

Serialization Flowchart

Example Code

The following class demonstrates the four typical cases that affect FastJSON serialization:

/**
 * case1: @JSONField(serialize = false)
 * case2: getXxx() returns void
 * case3: isXxx() returns non‑boolean
 * case4: @JSONType(ignores = "xxx")
 */
@JSONType(ignores = "otherName")
public class CountryDTO {
    private String country;

    public void setCountry(String country) { this.country = country; }
    public String getCountry() { return this.country; }
    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"; }

    public void getEnglishName3() { System.out.println("getEnglishName3() executed!!"); }

    @JSONField(serialize = false)
    public String isChinaName2() { System.out.println("isChinaName2() executed!!"); return "isChinaName2"; }
}

Running the test produces:

isChinaName() executed!!
getEnglishName() executed!!
{"chinaName":true,"englishName":"lucy"}

Code Standards

Serialization rules are numerous: return types, parameter counts, and annotations such as @JSONType and @JSONField all influence the outcome. To reduce ambiguity across team members, it is recommended to explicitly mark methods that should not be serialized with @JSONField(serialize = false). The revised CountryDTO code makes non‑serializable methods instantly recognizable.

Three Frequently Encountered Serialization Scenarios

The diagram below summarizes the typical workflow: discover the issue → analyze the underlying principle → resolve the problem → refine coding standards.

From a business perspective, the process guides developers from problem resolution to scalable solution design; from a technical perspective, it helps master the serialization pipeline for each isolated issue.

backend developmentserializationFastJSONASM
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.