Backend Development 12 min read

Random Fastjson Deserialization Failure Caused by Constructor Order

The article explains that Fastjson sometimes throws a syntax‑error exception when deserializing a JSON list of StewardTipCategory objects because the JVM returns overloaded constructors in nondeterministic order, causing Fastjson to pick the wrong constructor; removing or renaming the ambiguous constructor fixes the issue.

DaTaobao Tech
DaTaobao Tech
DaTaobao Tech
Random Fastjson Deserialization Failure Caused by Constructor Order

This article investigates a nondeterministic deserialization error that occurs when using the Fastjson library to parse a JSON string into Java objects.

Problem Statement

The JSON string represents a list of StewardTipCategory objects, each containing a list of StewardTipItem . During deserialization, Fastjson sometimes throws a JSONException: syntax error, expect {, actual [ exception.

Relevant Source Code

package test; import java.util.List; public class StewardTipItem { private Integer type; private List<String> contents; public StewardTipItem(Integer type, List<String> contents) { this.type = type; this.contents = contents; } }

package test; import java.util.ArrayList; import java.util.List; import java.util.Map; public class StewardTipCategory { private String category; private List<StewardTipItem> items; public StewardTipCategory build() { return null; } // C1 constructor public StewardTipCategory(String category, Map<Integer, List<String>> items) { List<StewardTipItem> categoryItems = new ArrayList<>(); for (Map.Entry<Integer, List<String>> item : items.entrySet()) { StewardTipItem tipItem = new StewardTipItem(item.getKey(), item.getValue()); categoryItems.add(tipItem); } this.items = categoryItems; this.category = category; } // C2 constructor public StewardTipCategory(String category, List<StewardTipItem> items) { this.category = category; this.items = items; } // getters and setters omitted for brevity }

package test; import java.util.ArrayList; import java.util.List; import java.util.Map; public class StewardTip { private List<StewardTipCategory> categories; public StewardTip(Map<String, Map<Integer, List<String>>> categories) { List<StewardTipCategory> tipCategories = new ArrayList<>(); for (Map.Entry<String, Map<Integer, List<String>>> category : categories.entrySet()) { StewardTipCategory tipCategory = new StewardTipCategory(category.getKey(), category.getValue()); tipCategories.add(tipCategory); } this.categories = tipCategories; } public StewardTip(List<StewardTipCategory> categories) { this.categories = categories; } // getters and setters omitted for brevity }

JSON used for testing:

{ "categories":[ { "category":"工艺类", "items":[ {"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1}, {"contents":["工艺类-疑问项-内容1"],"type":2} ] } ] }

Test driver:

package test; import com.alibaba.fastjson.JSONObject; public class FastJSONTest { public static void main(String[] args) { String tip = "{...}"; // the JSON above try { JSONObject.parseObject(tip, StewardTip.class); } catch (Exception e) { e.printStackTrace(); } } }

Stack Trace

com.alibaba.fastjson.JSONException: syntax error, expect {, actual [ at com.alibaba.fastjson.parser.deserializer.MapDeserializer.parseMap(MapDeserializer.java:228) ... (omitted for brevity) ... at test.FastJSONTest.main(FastJSONTest.java:17)

Root Cause Analysis

The failure is caused by the nondeterministic order of constructors returned by Class#getDeclaredConstructors() . Fastjson selects a constructor based on the order of this array. When the constructor C1 (which expects a Map<Integer, List<String>> ) is chosen first, Fastjson attempts to deserialize the items field with a MapDeserializer , which fails because the actual field type is List<StewardTipItem> . When the constructor C2 (which expects a List<StewardTipItem> ) appears first, deserialization succeeds.

The table below (excerpt) shows experimental results of different constructor orders:

Order -> Result build() C1 C2 -> Random C1 build() C2 -> C2,C1 (success) C1 C2 build() -> C2,C1 (success) C2 C1 build() -> Random

Solution

1. Remove the ambiguous C1 constructor or rename it to avoid being selected by Fastjson. 2. Ensure constructor parameter names and types match the JSON structure exactly. 3. For debugging, the following snippet prints the constructor order and forces a failure when C1 is first:

package test; import com.alibaba.fastjson.JSONObject; import java.lang.reflect.Constructor; public class FastJSONTest { public static void main(String[] args) { Constructor [] declaredConstructors = StewardTipCategory.class.getDeclaredConstructors(); if ("public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)".equals(declaredConstructors[0].toGenericString())) { String tip = "{...}"; // same JSON try { JSONObject.parseObject(tip, StewardTip.class); } catch (Exception e) { e.printStackTrace(); } } } }

Takeaways

Avoid overloaded constructors with different parameter types for the same logical purpose; they can introduce nondeterministic behavior in reflection‑based libraries.

Frameworks should validate not only field names and counts but also field types during deserialization.

Understanding JVM internals such as Class#getDeclaredConstructors is essential for diagnosing obscure bugs.

By aligning constructor signatures with the expected JSON schema and removing ambiguous overloads, the Fastjson deserialization becomes stable and deterministic.

BackendJavareflectionfastjsondeserializationConstructorOrderDebugging
DaTaobao Tech
Written by

DaTaobao Tech

Official account of DaTaobao Technology

0 followers
Reader feedback

How this landed with the community

login 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.