jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Inconsistent behavior for Double.NaN as map value in case of a standalone map and object property

Open mlwida opened this issue 2 years ago • 3 comments

Describe the bug Writing and subsequent reading of Double.NaN as map value succeeds for a standalone Map, but fails if the map is an object property. A user in the jackson-user-group suggested to file a bug report.

Version information 2.13.0

To Reproduce

Here are the Unit-Tests

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;
 
class JacksonNaNMapValueExample
{

	private static class SimpleClass
	{
		private Map<String, Double> map;

		public SimpleClass() {}

		public SimpleClass setMap(Map<String, Double> map) {this.map = map;return this;}

		public Map<String, Double> getMap() {return map;}

		@Override public String toString()
		{
			return "SimpleClass{" + "map=" + map + '}';
		}
	}
	
	private static ObjectMapper mapper;
	
	@BeforeAll 
	static void setup(){
		mapper = new ObjectMapper();
		mapper.enable(SerializationFeature.INDENT_OUTPUT). //
				setSerializationInclusion(JsonInclude.Include.NON_NULL). // 
				disable(SerializationFeature.FAIL_ON_EMPTY_BEANS). //
				configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false). //
				setVisibility( 
				mapper.getSerializationConfig() //
						.getDefaultVisibilityChecker() //
						.withFieldVisibility(JsonAutoDetect.Visibility.ANY)  //
						.withGetterVisibility(JsonAutoDetect.Visibility.NONE)); //
		PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class).build();
		mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING);
	}
	
	@Test
	void testMapWithDoubleNaNStandalone() 
	{
		Map<String, Double> map = new HashMap<>();
		map.put("A", Double.NaN);
		map.put("B", Double.POSITIVE_INFINITY);
		
		Map<String, Double> restoredMap = new HashMap<>();
		Assertions.assertDoesNotThrow(() -> {
			String jsonString = mapper.writeValueAsString(map);
			restoredMap.putAll(mapper.readValue(jsonString, Map.class));
		});
		Assertions.assertEquals(Double.NaN,restoredMap.get("A"));
		Assertions.assertEquals(Double.POSITIVE_INFINITY,restoredMap.get("B"));
	}

	@Test
	void testMapWithDoubleNaNAsObjectProperty() 
	{
		Map<String, Double> map = new HashMap<>();
		map.put("A", Double.NaN);
		map.put("B", Double.POSITIVE_INFINITY);
		SimpleClass obj = new SimpleClass();
		obj.setMap(map);
		
		Map<String, Double> restoredMap = new HashMap<>();
		Assertions.assertDoesNotThrow(() -> {
			String jsonString = mapper.writeValueAsString(obj);
			restoredMap.putAll( mapper.readValue(jsonString, SimpleClass.class).getMap());
		});
		Assertions.assertEquals(Double.NaN,restoredMap.get("A"));
		Assertions.assertEquals(Double.POSITIVE_INFINITY,restoredMap.get("B"));
	}
	
}

Expected behavior

Both Unit-Tests should succeed or fail. If jackson is working as expected, I kindly request an explanation :)

Additional context

If

ObjectMapper.DefaultTyping.EVERYTHING

is replaced with

ObjectMapper.DefaultTyping.NON_FINAL

both tests succeed.

mlwida avatar Nov 24 '21 13:11 mlwida

First things first: one thing I try to emphasize every chance I get is to please do not use generic Maps or Collections (or even POJOs) as the Root Value -- ever. This tends to lead to issues with Java Type Erasure which is what happens here: because JDK only offers type information for root Map as Map<?, ?>, polymorphic type information tends not to work as well. But when said Map is a POJO property, type declaration (Map<String, Double>) is available from class definition.

Whether this is what causes specific issue here I don't know; I'll see if I can figure this out when I have time to look into it. But it is something to consider in general.

cowtowncoder avatar Dec 16 '21 04:12 cowtowncoder

EVERYTHING: it tends to add Type Ids everywhere,even in cases where type can not be anything other than declared,Double 's declared value type is final, it results in adding type information in many places where it should not be needed,so,an error occurred

JavaCodeWking avatar Nov 03 '22 00:11 JavaCodeWking

@JavaCodeWking I recommend against ever using EVERYTHING for anything because it does what it says -- adds type id (almost) everywhere. I wish I had not added this choice in the first place, but it was requested by multiple users. But it does what it says so only use it if you really know you want it, for some reason.

cowtowncoder avatar Nov 03 '22 02:11 cowtowncoder