ObjectMapper convert class to map

45 Views Asked by At

ObjectMapper convert class to map , I'm in trouble. I have a util class. like this.

    @Slf4j
    public class JsonUtil {
        private static final ObjectMapper objectMapper;
        static {
            objectMapper = new ObjectMapper();
            JavaTimeModule javaTimeModule = new JavaTimeModule();
            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            objectMapper.registerModule(javaTimeModule);
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        }
        public static Map<String, Object> toMap(Object obj) {
            try {
                return objectMapper.convertValue(obj, new TypeReference<>() {
                });
            } catch (Exception e) {
                log.error("failed:", e);
                return Map.of();
            }
        }
    
        public static Map<String, Object> toMapIgnoreNull(Object obj) {
            try {
                return objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
                        .convertValue(obj, new TypeReference<>() {
                        });
            } catch (Exception e) {
                log.error("failed:", e);
                return Map.of();
            }
        }
    }

toMapIgnoreNull method appears to be converting an object to a map while ignoring null values. It uses an ObjectMapper from Jackson to achieve this. Here's a breakdown of what it does:

  1. It sets the serialization inclusion to NON_NULL, which means that null values will not be included in the resulting JSON.

  2. It converts the given object to a map using convertValue method of the ObjectMapper.

If any exception occurs during this process, it catches the exception, logs the error message, and returns an empty map.

This toMap method is also used to convert an object into a map, but it doesn't ignore null values. It also utilizes the ObjectMapper from the Jackson library. Here's a detailed explanation of the method:

  1. It uses the convertValue method of the ObjectMapper to convert the given object into a map.

  2. If any exceptions occur during this process, it catches the exception, logs the error message, and returns an empty map.

Compared to the previous method, this one doesn't filter out null values but retains all key-value pairs from the object, even if the value is null.

when

@Data
    @EqualsAndHashCode(callSuper = false)
    static class User {
        private String username;
        private String password;
    }

    public static void johnOut() {
        User john = new User();
        john.setUsername("John");
        Map<String, Object> johnMap = toMap(john);
        System.out.println("result 1: " + johnMap);
        Map<String, Object> JohnIgnoreNullMap = toMapIgnoreNull(john);
        System.out.println("result 2: " + JohnIgnoreNullMap);
    }

    private static void jimOut() {
        User jim = new User();
        jim.setUsername("jim");
        Map<String, Object> jimIgnoreNullMap = toMapIgnoreNull(jim);
        System.out.println("result 3: " + jimIgnoreNullMap);
        Map<String, Object> jimMap = toMap(jim);
        System.out.println("result 4: " + jimMap);
    }

    public static void main(String[] args) {
        johnOut();
        jimOut();
    }
when `johnOut()` is in front, the console prints:

result 1: {username=John, password=null}
result 2: {username=John, password=null}
result 3: {username=jim, password=null}
result 4: {username=jim, password=null}

but when `jimOut()` is in front, the console prints:

result 3: {username=jim}
result 4: {username=jim}
result 1: {username=John}
result 2: {username=John}

why?

expectation:
result 1: {username=John, password=null}
result 2: {username=John, password=null}
result 3: {username=jim}
result 4: {username=jim}
or

result 3: {username=jim}
result 4: {username=jim}
result 1: {username=John, password=null}
result 2: {username=John, password=null}

Is there any experience that can help me?
3

There are 3 best solutions below

0
boze-noob On

To ensure that each method call operates independently without affecting each other's settings, you need to create a new ObjectMapper instance within each method call. This way, each method has its own ObjectMapper instance with its own settings, and changes made in one method won't affect the behavior of the other method.

Here is helpful answer to the similar question: https://stackoverflow.com/a/37409580/12403892

0
Malvin Lok On

The reason you're seeing this behavior is because of the way ObjectMapper is set up in your JsonUtil class. The ObjectMapper instance is set up as a static variable, which means it's shared across all invocations of your toMap and toMapIgnoreNull methods.

The setSerializationInclusion(JsonInclude.Include.NON_NULL) function changes the state of the ObjectMapper – once you call it, all subsequent serializations done with that same ObjectMapper will also ignore null values unless you specifically reset it.

You're seeing different results in your johnOut() and jimOut() because the execution order determines whether the ObjectMapper is set to exclude nulls or not.

Here is an improved version of your JsonUtil class:

@Slf4j
public class JsonUtil {
    private static final ObjectMapper objectMapper;
    static {
        objectMapper = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        objectMapper.registerModule(javaTimeModule);
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
    
    public static Map<String, Object> toMap(Object obj) {
        try {
            return objectMapper.convertValue(obj, new TypeReference<>() {});
        } catch (Exception e) {
            log.error("failed:", e);
            return Map.of();
        }
    }

    public static Map<String, Object> toMapIgnoreNull(Object obj) {
        try {
            ObjectMapper localMapper = objectMapper.copy();
            localMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            return localMapper.convertValue(obj, new TypeReference<>() {});
        } catch (Exception e) {
            log.error("failed:", e);
            return Map.of();
        }
    }
}

In the toMapIgnoreNull method, I created a local copy of ObjectMapper to prevent it from affecting any other invocations of toMapIgnoreNull or toMap. Now, your johnOut() and jimOut() methods can be arranged in any order, and they should produce predictable results according to the methods used.

1
Thomas On

As others already commented your two methods are using the same object mapper and thus are sharing its settings. To fix this, create 2 object mappers and have each method use its own.

However, don't create a copy inside toMapIgnoreNull() because that seems wasteful. Instead create another shared instance just for that method.

What also intrigued me is the fact that you always get null or never, depending on the order. I'd have expected at least one password=null when johnOut() is called first. So I checked and it seems ObjectMapper is caching some settings which is why after being used for the first time some settings don't take effect (also see Jackson ObjectMapper setSerializationInclusion() not working).

That once more reinforces the statement above: if you have different configurations prepare a separate ObjectMapper instance for each config before using them.