Using FastJson to convert between hump and underscore and its misunderstandings

PropertyNamingStrategy

There are four serialization methods.
CamelCase strategy, Java object attribute: personId, attribute after serialization: personId – actually only the first letter is changed from uppercase to lowercase
PascalCase strategy, Java object attribute: personId, attribute after serialization: PersonId – actually only the first letter is changed from lowercase to uppercase
SnakeCase strategy, Java object attribute: personId, attribute after serialization: person_id -- add an underscore before the uppercase letter
KebabCase strategy, Java object attribute: personId, attribute after serialization: person-id - plus and minus signs before uppercase letters

public enum PropertyNamingStrategy {
                                    CamelCase, //hump
                                    PascalCase, //
                                    SnakeCase, //Underscore before capital letters 
                                    KebabCase;

    public String translate(String propertyName) {
        switch (this) {
            case SnakeCase: {
                StringBuilder buf = new StringBuilder();
                for (int i = 0; i < propertyName.length(); ++i) {
                    char ch = propertyName.charAt(i);
                    if (ch >= 'A' && ch <= 'Z') {
                        char ch_ucase = (char) (ch + 32);
                        if (i > 0) {
                            buf.append('_');
                        }
                        buf.append(ch_ucase);
                    } else {
                        buf.append(ch);
                    }
                }
                return buf.toString();
            }
            case KebabCase: {
                StringBuilder buf = new StringBuilder();
                for (int i = 0; i < propertyName.length(); ++i) {
                    char ch = propertyName.charAt(i);
                    if (ch >= 'A' && ch <= 'Z') {
                        char ch_ucase = (char) (ch + 32);
                        if (i > 0) {
                            buf.append('-');
                        }
                        buf.append(ch_ucase);
                    } else {
                        buf.append(ch);
                    }
                }
                return buf.toString();
            }
            case PascalCase: {
                char ch = propertyName.charAt(0);
                if (ch >= 'a' && ch <= 'z') {
                    char[] chars = propertyName.toCharArray();
                    chars[0] -= 32;
                    return new String(chars);
                }

                return propertyName;
            }
            case CamelCase: {
                char ch = propertyName.charAt(0);
                if (ch >= 'A' && ch <= 'Z') {
                    char[] chars = propertyName.toCharArray();
                    chars[0] += 32;
                    return new String(chars);
                }

                return propertyName;
            }
            default:
                return propertyName;
        }
    }

What works is the translate method

Specify the serialization format

After understanding PropertyNamingStrategy, see how it works,
Read the source code and find that when buildingBeanInfo (note that when converting bean s to json and building json information, if it is a map, JSONObject will not have this conversion)

    if(propertyNamingStrategy != null && !fieldAnnotationAndNameExists){
                    propertyName = propertyNamingStrategy.translate(propertyName);
                }

Here call the method corresponding to PropertyNamingStrategy respectively

Common Misunderstandings
That is to say, setting the output format through PropertyNamingStrategy is only valid for javaBean, and, as for the conversion result, it needs to be analyzed according to the content of the PropertyNamingStrategy#translate method
If the fields in the javaBean are separated by underscores, then specifying CamelCase for serialization cannot be converted to camel case!
E.g

        Student student = new Student();
        student.setTest_name("test");
        SerializeConfig serializeConfig = new SerializeConfig();
        serializeConfig.setPropertyNamingStrategy(PropertyNamingStrategy.CamelCase);
        System.out.println(JSON.toJSONString(student,serializeConfig));

Output {test_name": "test"}, because the CamelCase of PropertyNamingStrategy#translate is executed, just to judge if the first letter is converted from uppercase to lowercase. It cannot be completed, the conversion from underscore to camelcase

 case CamelCase: {
                char ch = propertyName.charAt(0);
                if (ch >= 'A' && ch <= 'Z') {
                    char[] chars = propertyName.toCharArray();
                    chars[0] += 32;
                    return new String(chars);
                }

                return propertyName;
            }

Specify the deserialization format

Smart matching function

When fastjson deserializes, it can automatically convert underscore to camel case. This is very convenient. , no matter which form is used during deserialization, it can match successfully and set the value

        String str = "{'user_name':123}";
        User user = JSON.parseObject(str, User.class);
        System.out.println(user);

output {userName='123'}

fastjson intelligent matching process

When fastjson is deserializing, when parsing the key value of each json field, it will call
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField
this way

Taking the above example as an example, set a breakpoint through debug to see the processing logic when parsing user_id.
At this time, the key in this method is user_id, and the object is the result object to be deserialized. In this example, it is FastJsonTestMain.UserInfo

    public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
                              Map<String, Object> fieldValues, int[] setFlags) {
        JSONLexer lexer = parser.lexer; // xxx
        //Whether to disable smart matching;
        final int disableFieldSmartMatchMask = Feature.DisableFieldSmartMatch.mask;
        final int initStringFieldAsEmpty = Feature.InitStringFieldAsEmpty.mask;
        FieldDeserializer fieldDeserializer;
        if (lexer.isEnabled(disableFieldSmartMatchMask) || (this.beanInfo.parserFeatures & disableFieldSmartMatchMask) != 0) {
            fieldDeserializer = getFieldDeserializer(key);
        } else if (lexer.isEnabled(initStringFieldAsEmpty) || (this.beanInfo.parserFeatures & initStringFieldAsEmpty) != 0) {
            fieldDeserializer = smartMatch(key);
        } else {
            //Do smart matching
            fieldDeserializer = smartMatch(key, setFlags);
        }
    
    ***omitted here N Multi-line***
    }

Look at the core code again, intelligent matching smartMatch

public FieldDeserializer smartMatch(String key, int[] setFlags) {
        if (key == null) {
            return null;
        }
        
        FieldDeserializer fieldDeserializer = getFieldDeserializer(key, setFlags);

        if (fieldDeserializer == null) {
            if (this.smartMatchHashArray == null) {
                long[] hashArray = new long[sortedFieldDeserializers.length];
                for (int i = 0; i < sortedFieldDeserializers.length; i++) {
                	//The nameHashCode of the java field, see the source code below
                    hashArray[i] = sortedFieldDeserializers[i].fieldInfo.nameHashCode;
                }
                //Get the field name hashcode value of the deserialized target object and sort it
                Arrays.sort(hashArray);
                this.smartMatchHashArray = hashArray;
            }

            // smartMatchHashArrayMapping
            long smartKeyHash = TypeUtils.fnv1a_64_lower(key);
            //Perform binary search to determine whether to find
            int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
            if (pos < 0) {
                //The original field is not matched, use fnv1a_64_extract to process it and match again
                long smartKeyHash1 = TypeUtils.fnv1a_64_extract(key);
                pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash1);
            }

            boolean is = false;
            if (pos < 0 && (is = key.startsWith("is"))) {
                //After the above operation, there is still no match, remove is and match again
                smartKeyHash = TypeUtils.fnv1a_64_extract(key.substring(2));
                pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
            }

            if (pos >= 0) {
                //Successfully matched through the smart matching field
                if (smartMatchHashArrayMapping == null) {
                    short[] mapping = new short[smartMatchHashArray.length];
                    Arrays.fill(mapping, (short) -1);
                    for (int i = 0; i < sortedFieldDeserializers.length; i++) {
                        int p = Arrays.binarySearch(smartMatchHashArray, sortedFieldDeserializers[i].fieldInfo.nameHashCode);
                        if (p >= 0) {
                            mapping[p] = (short) i;
                        }
                    }
                    smartMatchHashArrayMapping = mapping;
                }

                int deserIndex = smartMatchHashArrayMapping[pos];
                if (deserIndex != -1) {
                    if (!isSetFlag(deserIndex, setFlags)) {
                        fieldDeserializer = sortedFieldDeserializers[deserIndex];
                    }
                }
            }

            if (fieldDeserializer != null) {
                FieldInfo fieldInfo = fieldDeserializer.fieldInfo;
                if ((fieldInfo.parserFeatures & Feature.DisableFieldSmartMatch.mask) != 0) {
                    return null;
                }

                Class fieldClass = fieldInfo.fieldClass;
                if (is && (fieldClass != boolean.class && fieldClass != Boolean.class)) {
                    fieldDeserializer = null;
                }
            }
        }


        return fieldDeserializer;
    }

It can be seen from the above smartMatch method that the reason why the underscore can be automatically converted to hump in fastjson is mainly because the fnv1a_64_lower and fnv1a_64_extract methods are used for field comparison.
fnv1a_64_extract method source code:

    public static long fnv1a_64_extract(String key) {
        long hashCode = fnv1a_64_magic_hashcode;
        for (int i = 0; i < key.length(); ++i) {
            char ch = key.charAt(i);
            //Remove underscores and minus signs
            if (ch == '_' || ch == '-') {
                continue;
            }
            //uppercase to lowercase
            if (ch >= 'A' && ch <= 'Z') {
                ch = (char) (ch + 32);
            }
            hashCode ^= ch;
            hashCode *= fnv1a_64_magic_prime;
        }
        return hashCode;
    }

As can be seen from the source code, the fnv1a_64_extract method mainly does this:
Remove underscores, minus signs, and convert uppercase to lowercase
Summarize
The principle of intelligent field matching in fastjson is to use the TypeUtils.fnv1a_64_lower method to convert all fields to lowercase during field matching.
Then use the TypeUtils.fnv1a_64_extract method to remove the "_" and "-" symbols from the json field, and then convert all of them to lowercase.
If the above operation still fails to match successfully, it will perform another match by removing the is in the json field.
If the above operation still fails to match successfully, it will perform another match by removing the is in the json field.

When Smart Matching is turned off

It is turned on by default during smart matching, and needs to be turned off manually, see this example

 String str = "{'user_name':123}";
        ParserConfig parserConfig = new ParserConfig();
        parserConfig.propertyNamingStrategy =  PropertyNamingStrategy.SnakeCase;
        User user = JSON.parseObject(str, User.class, parserConfig,Feature.DisableFieldSmartMatch);
        System.out.println(user);

output {userName='null'}
So how to complete the conversion from underscore to camel case in this case
Then you need to use parseConfig

        String str = "{'user_name':123}";
        ParserConfig parserConfig = new ParserConfig();
        parserConfig.propertyNamingStrategy =  PropertyNamingStrategy.SnakeCase;
        User user = JSON.parseObject(str, User.class,parserConfig,Feature.DisableFieldSmartMatch);
        System.out.println(user);

So how does PropertyNamingStrategy.SnakeCase work at this time?
Breakpoint PropertyNamingStrategy#translate method
Found that when building the JavaBeanDeserializer

public JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type){
        this(config //
                , JavaBeanInfo.build(clazz, type, config.propertyNamingStrategy, config.fieldBased, config.compatibleWithJavaBean, config.isJacksonCompatible())
        );
    }
  if (propertyNamingStrategy != null) {
                propertyName = propertyNamingStrategy.translate(propertyName);
            }

            add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
                    annotation, fieldAnnotation, null, genericInfo));

The propertyName will be translate d according to the configuration. The attribute name converted to the corresponding format

Common mistakes:
The same as the serialization misunderstanding, if it is a map, JSONObject will not have this conversion, and the conversion result needs to be viewed by referring to the logic of the translate method
It is worth noting that the toJavaObject method of JSONObject, smart matching will take effect. You can safely convert between underscore and camel case

        String str = "{'user_name':123}";
        JSONObject object = (JSONObject) JSON.parse(str);
        System.out.println(object);
        User user = object.toJavaObject(User.class);
        System.out.println(user);

Tags: Java IDE JSON

Posted by mark_nsx on Fri, 30 Dec 2022 22:18:26 +0530