2

I have 2 objects that I'm trying to diff. For this purpose I use Jackson ObjectMapper in spring-boot version 2.1.3 to de-serialize them to a String, read them as a tree (convert to JsonNode) and then diff them to create a JsonPatch object. What I do notice is that in the existing object's JsonNode all the fields having null values are skipped as are objects having only null valued fields. The ObjectMapper seems to be behaving differently in the consumer (spring-boot version 2.6.7) and service provider (spring-boot-2.1.3). Sample, within the tree:

"_optionalAttrs":{"styles":{"99_0002_4_24_002":{"hineck":null,"choices":{"99_0002_4_24_002_001":{"color":null}}},"99_0002_4_24_001":{"hineck":null,"choices":null}}.   

vs

"_optionalAttrs":{"styles":{"99_0002_4_24_002":{"choices":{"99_0002_4_24_002_001":{}}},"99_0002_4_24_001":{}}

I am guessing this is the reason why the JsonPatch operations generated are incorrect :

op: copy; from: "/_optionalAttrs/styles/99_0002_4_24_001/hineck"; path: "/_optionalAttrs/clientAttributes/channel"

and I end up getting this error - no such path in target JSON document. Is there a way to ensure that the two remain consistent? If you think there are any other issues please let me know. The consumer code is something we can change but the service provider code is not in our ownership. I am using json-patch-1.12 and jdk 11.

2 Answers 2

1

By default, ObjectMapper will keep null values, but you can skip them using the following setting:

objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
Sign up to request clarification or add additional context in comments.

1 Comment

Yeah, I just found out that our service provider uses this objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); and we don't in our consumer code. Changing it did the trick.
0

I have not good solution, but it's better then nothing You can map object to Map<String, String> with fieldAndValueMap method and compare them

    public Map<String, String> fieldAndValueMap(Class<?> type, Object obj) throws ReflectiveOperationException {
        Map<String, String> result = new HashMap<>();
        for (Field field : type.getDeclaredFields()) {
            field.setAccessible(true);
            if (isWrapperType(field.getType())) {
                String value = nonNull(field.get(obj)) ? field.get(obj).toString() : "null";
                result.put(type.getSimpleName() + "." + field.getName(), value);
            } else {
                String getMethod = "get" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
                Method method = type.getMethod(getMethod);
                Object subObj = method.invoke(obj);
                if (nonNull(subObj)) {
                    result.putAll(fieldAndValueMap(field.getType(), subObj));
                }
            }
        }
        return result;
    }

    public boolean isWrapperType(Class<?> type) {
        return getWrapperTypes().contains(type);
    }

    private Set<Class<?>> getWrapperTypes() {
        Set<Class<?>> ret = new HashSet<>();
        ret.add(Boolean.class);
        ret.add(Character.class);
        ret.add(String.class);
        ret.add(Byte.class);
        ret.add(Short.class);
        ret.add(Integer.class);
        ret.add(Long.class);
        ret.add(Float.class);
        ret.add(Double.class);
        ret.add(Void.class);
        ret.add(BigDecimal.class);
        ret.add(LocalDate.class);
        ret.add(LocalDateTime.class);
        ret.add(LocalTime.class);
        ret.add(Date.class);
        return ret;
    }
to get object as map of fields and values: fieldAndValueMap(firstObject.getClass(), firstObject)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.