About [pit avoidance description of precision loss during serialization and deserialization]

        1. background

When the two sides interact with each other through the HTTP network protocol, the sender often serializes the Json object data into a string (ObjectMapper.writeValueAsString(Object value)), while the receiver often deserializes the received string data into a Json object (objectmapper. readValue (string content, class ValueType)). Among them, when the value type appears in the Json object and the receiver is in readValue, the problem of scientific counting method and precision loss will occur in the value field.

This problem may occur in any framework involving serialization and deserialization. The jackson framework is used as a demonstration below.

        2. Scene reproduction

a. When the numeric type is floating-point number and reaches tens of millions, the scientific counting method problem will occur when the string is converted to Json object

b. When the value type is a floating point number and ends with "0", the conversion of string to Json object will "wipe 0" problem

         3. Solution

  • The data of numerical type agreed by both sides of the interaction also adopts string transmission, and the above two problems do not exist in string transmission.
  • In view of the problem of scientific counting method, the use of inverse sequence function can be enabled for ObjectMapper_ BIG_ DECIMAL_ FOR_ Floats configuration, which is off by default.
    • to configure:
      private static mapper;
      static{
          mapper = new ObjectMapper();
          mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
      }
    • verification:
  • For the problem of precision loss ("wipe 0"), you can also turn on the configuration
    • to configure:
      private static ObjectMapper mapper;
      static{
          mapper = new ObjectMapper();
          mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
          JsonNodeFactory jsonNodeFactory = JsonNodeFactory.withExactBigDecimals(true);
          mapper.setNodeFactory(jsonNodeFactory);
      }
    • verification:

        4. Source code analysis ("0" problem

  • Enter the readValue method of ObjectMapper: it directly calls the object of ObjectMapper_ Readmapandclose (jsonparser P0, javatype ValueType) method.
  • ObjectMapper's object_ Readmapandclose method:
  • In_ The deserialize(JsonParser p, DeserializationContext ctxt) method of calling JsonDeserializer is called in readMapAndClose method (JsonDeserialize is an abstract class). Since Class valueType parameter adopts JsonNode, the inheritance is inherited and the abstraction method is implemented.

  • Enter the deserialize method of JsonNodeDeserializer. It can be seen from the previous step that JsonToken is "START_OBJECT". In the deserialize method, the deserializeObject method of JsonNodeDeserializer is directly called.
//Source code of deserializeObject method of JsonNodeDeserializer:
protected final ObjectNode deserializeObject(JsonParser p, DeserializationContext ctxt,
        final JsonNodeFactory nodeFactory) throws IOException
{
    final ObjectNode node = nodeFactory.objectNode();
    String key = p.nextFieldName();
    for (; key != null; key = p.nextFieldName()) {
        JsonNode value;
        JsonToken t = p.nextToken();
        if (t == null) { // can this ever occur?
            t = JsonToken.NOT_AVAILABLE; // can this ever occur?
        }
        switch (t.id()) {
        case JsonTokenId.ID_START_OBJECT:
            value = deserializeObject(p, ctxt, nodeFactory);
            break;
        case JsonTokenId.ID_START_ARRAY:
            value = deserializeArray(p, ctxt, nodeFactory);
            break;
        case JsonTokenId.ID_EMBEDDED_OBJECT:
            value = _fromEmbedded(p, ctxt, nodeFactory);
            break;
        case JsonTokenId.ID_STRING:
            value = nodeFactory.textNode(p.getText());
            break;
        case JsonTokenId.ID_NUMBER_INT:
            value = _fromInt(p, ctxt, nodeFactory);
            break;
        case JsonTokenId.ID_TRUE:
            value = nodeFactory.booleanNode(true);
            break;
        case JsonTokenId.ID_FALSE:
            value = nodeFactory.booleanNode(false);
            break;
        case JsonTokenId.ID_NULL:
            value = nodeFactory.nullNode();
            break;
        default:
            value = deserializeAny(p, ctxt, nodeFactory);
        }
        JsonNode old = node.replace(key, value);
        if (old != null) {
            _handleDuplicateField(p, ctxt, nodeFactory,
                    key, node, old, value);
        }
    }
    return node;
}
  • Enter the deserializeObject method:
  • This method traverses all keys and values of the Json string we passed in. For example, we passed in {"key":123.10}. In the deserializeObject method, the nextToke method called JsonParser is called "VALUE_NUMBER_FLOAT", so we call JsonNode deserializeAny (JsonParser P, DeserializationContext ctxt, final final) again.

    From its source code, we can further call jsonnode_ Fromfloat (jsonparser P, deserializationcontext ctxt, final jsonnodefactory nodefactory) method.  

  • _ In the fromFloat method, a "DOUBLE" value type obtained from the getNumberType method of JsonParser. Therefore, the ValueNode numberNode(BigDecimal v) method of JsonNodeFactory will be called.

  • From the source code of numberNode, we know that decimalnode is called here Valueof (v.stripTrailingZeros()) method, while BigDecimal's stripTrailingZeros() method gets a value without the end of "0". The "wipe 0" phenomenon occurs here.
  • In the numberNode method, why not call decimalnode directly What about the valueof (V) method?
    • JsonNodeFactory_ cfgBigDecimalExact is false.
    • Source code:
      private final boolean _cfgBigDecimalExact;
      private static final JsonNodeFactory decimalsNormalized
          = new JsonNodeFactory(false);
      private static final JsonNodeFactory decimalsAsIs
          = new JsonNodeFactory(true);
      /**
       * Default singleton instance that construct "standard" node instances:
       * given that this class is stateless, a globally shared singleton
       * can be used.
       */
      public final static JsonNodeFactory instance = decimalsNormalized;
       
       
      public JsonNodeFactory(boolean bigDecimalExact)
      {
          _cfgBigDecimalExact = bigDecimalExact;
      }
      
      /**
       * Default constructor
       *
       * <p>This calls {@link #JsonNodeFactory(boolean)} with {@code false}
       * as an argument.</p>
       */
      protected JsonNodeFactory()
      {
          this(false);
      }
      
      /**
       * Return a factory instance with the desired behavior for BigDecimals
       * <p>See {@link #JsonNodeFactory(boolean)} for a full description.</p>
       *
       * @param bigDecimalExact see description
       * @return a factory instance
       */
      public static JsonNodeFactory withExactBigDecimals(boolean bigDecimalExact)
      {
          return bigDecimalExact ? decimalsAsIs : decimalsNormalized;
      }
    • From the source code of JsonNodeFactory, we can see that_ The assignment of cfgBigDecimalExact property is implemented by the constructor of JsonNode factory object or withExactBigDecimals method. (Note: the withexactbigdecisions method returns a new JsonNodeFactory object).
    • Moreover, it can be seen from the source code that JsonNodeFactory adopts the constructor of new JsonNodeFactory(false) by default, i.e_ cfgBigDecimalExact is false.
    • After we know the reason of "0" above, we can configure ObjectMapper:
      • Call the withexactbigdecisions() method of JsonNodeFactory and pass in the parameter "true" to get a new JsonNodeFactory object and reconfigure the NodeFactory of ObjectMapper.
      • to configure:
        private static ObjectMapper mapper;
        
        static{
            mapper = new ObjectMapper();
            mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
            JsonNodeFactory jsonNodeFactory = JsonNodeFactory.withExactBigDecimals(true);
            mapper.setNodeFactory(jsonNodeFactory);
            //perhaps
            //mapper.setConfig(mapper.getDeserializationConfig().with(jsonNodeFactory));
        }
  • In fact, setNodeFactory method is also the with(JsonNodeFactory f) method of DeserializationConfig, so mapper can also be used directly Setconfig().
    public ObjectMapper setNodeFactory(JsonNodeFactory f) {
        _deserializationConfig = _deserializationConfig.with(f);
        return this;
    }

    • The with method (JsonNodeFactory f) recreates a DeserializationConfig object. In this construction method, the JsonNodeFactory object is initialized with JsonNodeFactory(true). When JsonNodeFactory is not specified by default, JsonNodeFactory(false) is used for initialization (JsonNodeFactory.instance is used).
    • Source code:
      • When specifying JsonNodeFactory
      • When JsonNodeFactory is not specified

       

       

 

 

 

 

Keywords: Java

Added by Syntac on Thu, 10 Mar 2022 12:31:48 +0200