This week there is a requirement to call the ali cloud interface of a third party, the protocol parameter required by the other party must be capitalized. Normally, when we define beans, we do not directly set the variable name to uppercase, which is not in accordance with the coding specification. Is there a way to serialize the initial letter to uppercase string, which is passed as the request parameter? This type of requirement is accomplished mainly through some customization behavior of FastJson. At the same time, in the process, INCIDENTALLY read some fastjson source code, here for the record.

serialization

@Data
public static class Model {
  private int userId;
  private String userName;
}
Copy the code

Use code to verify the default serialization behavior.

Model model = new Model();
model.userId = 1001;
model.userName = "test";
System.out.println(JSON.toJSONString(model));
Copy the code

Output result:

{"userId":1001."userName":"test"}
Copy the code

You can see that the default serialization behavior is humped.

So if you want to implement the serialization of capital letters, how do you do it?

Scenario 1 specifies the configuration when serializing

Model model = new Model();
model.userId = 1001;
model.userName = "test";
// In production, config needs to be set to singleton, otherwise there will be performance issues
SerializeConfig serializeConfig = new SerializeConfig();
serializeConfig.propertyNamingStrategy = PropertyNamingStrategy.PascalCase;
String text = JSON.toJSONString(model, serializeConfig, SerializerFeature.SortField);
Copy the code

For the behavior of PropertyNamingStrategy see the FastJson issue github.com/alibaba/fas…

Output result:

{"UserId":1001."UserName":"test"}
Copy the code
Scenario 2 uses JSONField annotations to specify the field-level configuration
@Data
public static class ModelOne {
  @JSONField(name = "UserId")
  private int userId;
  @JSONField(name = "UserName")
  private String userName;
}
Copy the code

Test code:

ModelOne model = new ModelOne();
model.userId = 1001;
model.userName = "test";
String text = JSON.toJSONString(model, SerializerFeature.SortField);
System.out.println(text);
Copy the code

Output result:

{"UserId":1001."UserName":"test"}
Copy the code
Scenario 3 uses the JSONType annotation to specify the class-level configuration
@Data
@JSONType(naming = PropertyNamingStrategy.PascalCase)
public static class ModelTwo {
  private int userId;
  private String userName;
}
Copy the code

Test code:

ModelTwo model = new ModelTwo();
model.userId = 1001;
model.userName = "test";
String text = JSON.toJSONString(model, SerializerFeature.SortField);
System.out.println(text);
Copy the code

Output result:

{"UserId":1001."UserName":"test"}
Copy the code
Behavior when JSONType and JSONField are used in parallel
@Data
@JSONType(naming = PropertyNamingStrategy.PascalCase)
public static class ModelThree {
  private int userId;
  @JSONField(name = "userName")
  private String userName;
}
Copy the code

Test code:

ModelThree model = new ModelThree();
model.userId = 1001;
model.userName = "test";
String text = JSON.toJSONString(model, SerializerFeature.SortField);
System.out.println(text);
Copy the code

Example output:

{"UserId":1001."userName":"test"}
Copy the code

As you can see, if the two are used together, the JSONField on the field will dominate.

deserialization

After looking at serialization, is deserialization similar?

Our input strings are:

{\"UserId\":1001, \"UserName\":\"test\"}
Copy the code

Assertion validation:

Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
Copy the code
Default deserialization
@Data
public static class Model {
  private int userId;
  private String userName;
}

Model model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", Model.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
Copy the code
Specify the configuration when deserializing
@Data
public static class ModelZero {
  private int userId;
  private String userName;
}

// Generate environment, need to set to singleton, otherwise there will be performance issues
ParserConfig parserConfig = new ParserConfig();
parserConfig.propertyNamingStrategy = PropertyNamingStrategy.PascalCase;
Model model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", Model.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
// The test passes
Copy the code
After using the JSONField configuration, deserialize
@Data
public static class ModelOne {
  @JSONField(name = "UserId")
  private int userId;
  @JSONField(name = "UserName")
  private String userName;
}

ModelOne model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", ModelOne.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
Copy the code
After configuration with JSONType, deserialize
@Data
@JSONType(naming = PropertyNamingStrategy.PascalCase)
public static class ModelTwo {
  private int userId;
  private String userName;
}

ModelTwo model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", ModelTwo.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
Copy the code
Use JSONType and JSONField together

@Data
@JSONType(naming = PropertyNamingStrategy.PascalCase)
public static class ModelThree {
  private int userId;
  @JSONField(name = "userName")
  private String userName;
}

ModelThree model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", ModelThree.class);
Assert.assertEquals(1001, model2.userId);
Assert.assertEquals("test", model2.userName);
Copy the code

By running the Test case, we found that the deserialization verification code above passed, and it is very easy to understand the behavior of using JSONField and JSONType alone, because the input string is consistent with its own specification. But why does it work by default, including changing the field configuration to lowercase via @jsonField?

SmartMatch about FastJson

The core of the above problem lies in FastJson smartMatch, that is, FastJson intelligent detection, mapping UserId to UserId, UserName to UserName. The JavaBeanDeserializer method public FieldDeserializer smartMatch(String key, int[] setFlags) is used to verify the core source code.

Parse the source code:

public FieldDeserializer smartMatch(String key, int[] setFlags) {
  // key: Enter the field name, such as UserName or UserId
  if (key == null) {
    return null;
  }
	
  // The normal field deserializers are named uesrId and userName in camel case, so the correct deserializers cannot be found when the input is UserId or userName starts with uppercase letters.
  FieldDeserializer fieldDeserializer = getFieldDeserializer(key, setFlags);

  if (fieldDeserializer == null) {
    if (this.smartMatchHashArray == null) {
      // Generate a smart matching array based on existing normal serializers
      long[] hashArray = new long[sortedFieldDeserializers.length];
      for (int i = 0; i < sortedFieldDeserializers.length; i++) {
        hashArray[i] = sortedFieldDeserializers[i].fieldInfo.nameHashCode;
      }
      Arrays.sort(hashArray);
      this.smartMatchHashArray = hashArray;
    }

    // smartMatchHashArrayMapping
    Typeutils.fnval_64_lower calculates the hash value of the key
    //public static long fnv1a_64_lower(String key){
    // long hashCode = 0xcbf29ce484222325L;
    // for(int i = 0; i < key.length(); ++i){
    // char ch = key.charAt(i);
    // The hash value for userName is the same as the hash value for userName
    // if(ch >= 'A' && ch <= 'Z'){
    // ch = (char) (ch + 32);
    / /}
    // hashCode ^= ch;
    // hashCode *= 0x100000001b3L;
    / /}
    // return hashCode;
    // }
    // Convert uppercase letters to lowercase letters
    long smartKeyHash = TypeUtils.fnv1a_64_lower(key);
    // Find the corresponding location in the key of the existing deserializer
    int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
    if (pos < 0) {
      // If not, you need to see if there is an underscore or a hyphen
      // public static long fnv1a_64_extract(String key){
      // long hashCode = 0xcbf29ce484222325L;
      // for(int i = 0; i < key.length(); ++i){
      // char ch = key.charAt(i);
              // The calculation does not take into account the hyphen or underscore
      // if(ch == '_' || ch == '-'){
      // continue;
      / /}
              // Uppercase letters are converted to lowercase letters
      // if(ch >= 'A' && ch <= 'Z'){
      // ch = (char) (ch + 32);
      / /}
      // hashCode ^= ch;
      // hashCode *= 0x100000001b3L;
      / /}
      // return hashCode;
      / /}
      long smartKeyHash1 = TypeUtils.fnv1a_64_extract(key);
      pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash1);
    }

    boolean is = false;
    if (pos < 0 && (is = key.startsWith("is"))) {
      // For Boolean types, the usual get method starts with is and requires special handling
      smartKeyHash = TypeUtils.fnv1a_64_extract(key.substring(2));
      pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
    }

    if (pos >= 0) {
      // If the hash position of the normal deserializer is found according to the hash
      if (smartMatchHashArrayMapping == null) {
        // The following logic is mainly to get the location of each serializer
        short[] mapping = new short[smartMatchHashArray.length];
        Arrays.fill(mapping, (short) -1);
        for (int i = 0; i < sortedFieldDeserializers.length; i++) {
          // Iterate over all serializers
          int p = Arrays.binarySearch(smartMatchHashArray, sortedFieldDeserializers[i].fieldInfo.nameHashCode);
          if (p >= 0) {
            // Assign the key hash corresponding to position P, should use position I deserializers
            mapping[p] = (short) i;
          }
        }
        smartMatchHashArrayMapping = mapping;
      }
			
      int deserIndex = smartMatchHashArrayMapping[pos];
      if(deserIndex ! = -1) {
        if(! isSetFlag(deserIndex, setFlags)) {// Assign to the serializerfieldDeserializer = sortedFieldDeserializers[deserIndex]; }}}if(fieldDeserializer ! =null) {
      FieldInfo fieldInfo = fieldDeserializer.fieldInfo;
      // If disalbeFieldSmartMatch is set, null is returned
      if((fieldInfo.parserFeatures & Feature.DisableFieldSmartMatch.mask) ! =0) {
        return null;
      }

      Class fieldClass = fieldInfo.fieldClass;
      if(is && (fieldClass ! =boolean.class && fieldClass ! = Boolean.class)) {// Return null if is, but the type is not Boolean
        fieldDeserializer = null; }}}return fieldDeserializer;
}
Copy the code

Analysis of the source code, we can set up Feature. DisableFieldSmartMatch to see if you can solve our questions.

We validate the default behavior, and the other case is similar

ModelZero model2 = JSON.parseObject("{\"UserId\":1001, \"UserName\":\"test\"}", ModelZero.class, Feature.DisableFieldSmartMatch);
System.out.println(JSON.toJSONString(model2));
Copy the code

Output result:

// userName is not mapped, so null, no output.
// userId is not mapped and is the default value of int, 0
{"userId":0}
Copy the code

conclusion

  1. You can use SerializeConfig and ParserConfig to customize the behavior while performing the operation. However, it is important to note that in the production environment, it is set to singleton.
  2. You can use @jsonField for field level serialization and deserialization configuration.
  3. You can use @jsonType for class-level serialization and deserialization configuration, with a lower priority than @jsonField
  4. By default, FastJson does smartMatch when deserializing, which blocks differences in case, underscore, and underscore. UserName is the same as userName is the same as user-name. Can be specified through the serialization, Feature. DisableFieldSmartMatch closed this Feature.