linuxNoob
linuxNoob

Reputation: 730

JSON Patch diffing fails due to ObjectMapper?

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.

Upvotes: 2

Views: 1294

Answers (2)

Kirill Simonov
Kirill Simonov

Reputation: 8491

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

objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)

Upvotes: 1

Alisher Gulov
Alisher Gulov

Reputation: 71

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)

Upvotes: 0

Related Questions