Problem
When writing Unit Test in Java, the expected value of Unit Test is usually read in expectResult.json from the test/resources directory by tools and deserialized by JSON serialization tools to obtain the expected results of Unit Test, and compared with the actual results of the test. However, if the classes used for deserialization are derived from third-party libraries (i.e., no code can be changed), there will be great problems. Following is my description of the problems encountered in deserializing the StremRecord class of Aliyun.
The StreamRecord class is defined as follows:
public class StreamRecord { public enum RecordType { /** * PUT type * If the corresponding row already exists, the Record needs to overwrite the original data. */ PUT, /** * UPDATE type * If the corresponding row already exists, the Record is an update on the original data. */ UPDATE, /** * DELETE type * Indicates that the corresponding row is to be deleted. */ DELETE } /** * Record Types of */ private RecordType recordType; /** * Primary keys for rows */ private PrimaryKey primaryKey; /** * Timing information for rows */ private RecordSequenceInfo sequenceInfo; /** * The Record contains an attribute column of type RecordColumn */ private List<RecordColumn> columns; /** * Get the type of Record * @return Record Types of */ public RecordType getRecordType() { return recordType; } public void setRecordType(RecordType recordType) { this.recordType = recordType; } /** * Get the primary key for the corresponding row * @return Primary keys for rows */ public PrimaryKey getPrimaryKey() { return primaryKey; } public void setPrimaryKey(PrimaryKey primaryKey) { this.primaryKey = primaryKey; } /** * Get timing information for the row * @return Timing information for the row */ public RecordSequenceInfo getSequenceInfo() { return sequenceInfo; } public void setSequenceInfo(RecordSequenceInfo sequenceInfo) { this.sequenceInfo = sequenceInfo; } /** * Gets the list of attribute columns contained in the Record * @return The list of attribute columns contained in the Record */ public List<RecordColumn> getColumns() { if (columns != null) { return columns; } else { return new ArrayList<RecordColumn>(); } } public void setColumns(List<RecordColumn> columns) { this.columns = columns; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("[RecordType:]"); sb.append(this.recordType); sb.append("\n[RecordSequenceInfo:]"); sb.append(this.sequenceInfo); sb.append("\n[PrimaryKey:]"); sb.append(this.primaryKey); sb.append("\n[Columns:]"); for (RecordColumn column : this.getColumns()) { sb.append("("); sb.append(column); sb.append(")"); } return sb.toString(); } }
This project used Jackson to serialize and deserialize, but Jackson's ObjectMapper reported the error of No suitable constructor in deserializing this kind of structure. It was found that Jackson needs default constructor (if there is a constructor with parameters, it also needs to be modified with @JsonCreator). The constructor, which modifies the constructor parameters with @JsonProperty), does not have any of the above classes. Even if there are, we can not change third-party libraries such as Aliyun, so we abandon Jackson and consider Ali's own fastjson instead. Fastjson can deserialize this class, but when I carefully analyze the deserialized objects, I find that some deep fields have null values. After some investigation, I understand that although fastjson does not require constructors for deserialized classes, it requires fields, and deserialized private fields. A setter method can be used to deserialize normally (or to have a constructor with all field parameters). If a private field lacks a setter method, the value of the field is the default value. Finally, consider using Google's Gson, Gson does not have these problems, but if the deserialized class has an Object-type field whose value is numeric, then Gson will be Double-type, for example, if you have a field of
private Map<String, Object> map;
json file:
{ "age": 24, "height": 1.81 }
When the json file is deserialized into a map field, it is intuitively assumed that the value of the "age" field should be of Integer or Long type. However, Gson is somewhat abnormal. Because the value of the map is of Object type, it does not specify a specific numerical type explicitly. It will change the field whose key is "age" into Double type (and It's not the Integer or Long type that we intuitively expect, which brings trouble to subsequent programming. For this "feature" of Gson, you can refer to it. https://github.com/google/gso... The above "debate" is a more interesting "netizen's author".
My final solution is to use Gson deserialization, and then use ReflectionTestUtils.setField to convert some values from Double to Long.
Conclusion
- Jackson is powerful, but has a high requirement for deserialized classes (with default constructor s).
- Fastjson is fast, but there are certain requirements for deserialized classes, and there are many bug s when deserializing complex json (many SDKs in Aliyun do not use fastjson for this reason).
- Gson is comprehensive and requires the lowest deserialized classes, but it is not friendly enough to handle numeric fields of Object type.
The versions used in the above experiments are as follows:
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' compile group: 'com.alibaba', name: 'fastjson', version: '1.2.56'
Therefore, Jackson is preferred, or Gson is preferred, if there is no serialization or deserialization of the third-party library model (i.e., where the code cannot be changed).