alibaba / alibaba/fastjson2

[BUG] Deserialization from a json file and then Serialization back to json file produces incorrect result

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

Description

Code sample -
```
public static void main(String[] args) throws FileNotFoundException {
RunTimeConfig runTimeConfig =
JSON.parseObject(new FileReader("src/main/resources/fastjsontesting/RuntimeConfigVeryShort.json"),
RunTimeConfig.class);
JSON.writeTo(new FileOutputStream("src/main/resources/dummy.json"), runTimeConfig);
}
////////////////////////////////////////
package daytodaycode.model;

@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class RunTimeConfig extends Metadata implements Serializable {
private static final long serialVersionUID = 1L;
private String name;

private List transformConfigs;

@Override
public void compile() {}
}

////////////////////////////////////////
package daytodaycode.model;
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public abstract class Metadata implements Serializable {
private static final long serialVersionUID = 1L;

@EqualsAndHashCode.Exclude private OffsetDateTime lastMetadataRefreshTime;

/** Compile the metadata * */
public abstract void compile();
}

////////////////////////////////////////
package daytodaycode.model;

public interface TransformConfig extends DataFrameFunctionConfig {
String getInputDataFrameName();
}

////////////////////////////////////////
package daytodaycode.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;

@AllArgsConstructor
@Builder
@Getter
@ToString
@EqualsAndHashCode
public class ChangeEventTransformConfig implements TransformConfig{

private static final long serialVersionUID = 1L;
private final String inputDataFrameName;
private final String outputDataFrameName;
private final String tableName;

}
////////////////////////////////////////
package daytodaycode.model;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
@JsonTypeInfo(
use = Id.CLASS,
property = "type"
)
public interface DataFrameFunctionConfig {
String getOutputDataFrameName();
}
```
RuntimeConfigVeryShort.json file
```
{
"name": "test",
"transformConfigs": [
{
"type": "daytodaycode.model.ChangeEventTransformConfig",
"inputDataFrameName": "change_df",
"outputDataFrameName": "change_df_transformed",
"tableName": "change_table"
}
]
}
```
Expected Output -
```
{
"name": "test",
"transformConfigs": [
{
"type": "daytodaycode.model.ChangeEventTransformConfig",
"inputDataFrameName": "change_df",
"outputDataFrameName": "change_df_transformed",
"tableName": "change_table"
}
]
}
```
Actual Output -
```
{
"name": "test",
"transformConfigs": [
{
"inputDataFrameName": "change_df",
"outputDataFrameName": "change_df_transformed"
}
]
}
```
FASTJson2 version --> 2.0.37
I went through the code and found these problematic pieces,
In ObjectReaderCreator class, under createObjectReader method, the following piece of code creates an instance for ObjectReaderInterface.

```
if (objectClass.isInterface()) {
return new ObjectReaderInterface(
objectClass,
null,
null,
0L,
null,
null,
fieldReaderArray
);
}
```
There is no beanInfo.typeKey reference in the parameters. So the type information is lost. The below change adds the information from beanInfo class to the parameters.
```
if (objectClass.isInterface()) {
return new ObjectReaderInterface(
objectClass,
beanInfo.typeKey,
null,
beanInfo.readerFeatures,
null,
null,
fieldReaderArray
);
}
```
The above is required for this condition in ObjectReaderInterface class
```
JSONReader.Context context = jsonReader.getContext();
long features3, hash = jsonReader.readFieldNameHashCode();
JSONReader.AutoTypeBeforeHandler autoTypeFilter = context.getContextAutoTypeBeforeHandler();
if (i == 0
&& hash == getTypeKeyHash()
&& ((((features3 = (features | getFeatures() | context.getFeatures())) & JSONReader.Feature.SupportAutoType.mask) != 0) || autoTypeFilter != null)
)
```
In ObjectReaderBaseModule.ReaderAnnotationProcessor class, there are two changes required. The first is handling interfaces and the other is for superClass in the getBeanInfo method. Below are the rough changes I have made. The superClass code piece is required to handle any annotation on the super class. Right now if I take an abstract class and add an annotation to that, it is not recognized (for example create an abstract class called AbstractDataFrameFunctionConfig and add the @JsonTypeInfo annotation to that and then try to deserialize it. Keep the hierarchy the same, just change it from interface to abstract)
```
Class superClass = objectClass.getSuperclass();
if (superClass != null && superClass != Object.class && superClass != Enum.class) {
getBeanInfo(beanInfo, superClass);
}
Class[] interfaces = objectClass.getInterfaces();
if (interfaces.length > 0) {
for (Class superInterface : interfaces) {
getBeanInfo(beanInfo, superInterface);
}
}
```
In ObjectWriterBaseModule.WriterAnnotationProcessor getBeanInfo method added the following change
```
if (objectClass != null) {
Class superclass = objectClass.getSuperclass();
if (superclass != Object.class && superclass != null && superclass != Enum.class) {
getBeanInfo(beanInfo, superclass);

if (beanInfo.seeAlso != null && beanInfo.seeAlsoNames != null) {
for (int i = 0; i < beanInfo.seeAlso.length; i++) {
Class seeAlso = beanInfo.seeAlso[i];
if (seeAlso == objectClass && i < beanInfo.seeAlsoNames.length) {
String seeAlsoName = beanInfo.seeAlsoNames[i];
if (seeAlsoName != null && seeAlsoName.length() != 0) {
beanInfo.typeName = seeAlsoName;
break;
}
}
}
}
}
Class[] interfaces = objectClass.getInterfaces();
if (interfaces.length > 0) {
for (Class superInterface : interfaces) {
getBeanInfo(beanInfo, superInterface);

if (beanInfo.seeAlso != null && beanInfo.seeAlsoNames != null) {
for (int i = 0; i < beanInfo.seeAlso.length; i++) {
Class seeAlso = beanInfo.seeAlso[i];
if (seeAlso == objectClass && i < beanInfo.seeAlsoNames.length) {
String seeAlsoName = beanInfo.seeAlsoNames[i];
if (seeAlsoName != null && seeAlsoName.length() != 0) {
beanInfo.typeName = seeAlsoName;
break;
}
}
}
}
}
}
}
```
With the above changes, I am able to get the expected output.

Contributor guide

Open the contributing guide

Research direction

Reproduce the provided RunTimeConfig JSON round trip with fastjson2 2.0.37, then inspect ObjectReaderCreator.createObjectReader and the getBeanInfo methods in ObjectReaderBaseModule.ReaderAnnotationProcessor and ObjectWriterBaseModule.WriterAnnotationProcessor. Done means serialization preserves the type and tableName fields shown in the expected output.

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
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.