Search code examples
javajsonspringjson-patch

Jackson deserialise JsonPatch with object value


I'mu using JsonPatch (JSR-374) with implementation from Apache org.apache.johnzon:johnzon-core:1.2.4 in my Spring project PATCH endpoint:

@Bean
@Primary
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper objectMapper = builder.createXmlMapper(false).build();
    objectMapper.registerModule(new Jdk8Module());
    objectMapper.registerModule(new JSR353Module());
    return objectMapper;
}

Controller

@PatchMapping("/settings")
public ResponseEntity<SettingsResponse> patchSettings(@RequestBody JsonPatch patchDocument, Locale locale) {...}

With json request of a simple atomic values

[
  { "op": "replace", "path": "/currency", "value": "EUR" },
  { "op": "test", "path": "/version", "value": 10 }
]

JsonPatch instance is deserialised correctly by Jackson

But with complex value type (object):

[
  { "op": "replace", "path": "/currency", "value": {"code": "USD", "label": "US Dollar"} },
  { "op": "test", "path": "/version", "value": 10 }
]

Exception is thrown

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of javax.json.JsonPatch (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information at [Source: (PushbackInputStream); line: 1, column: 1]

I recon JsonPatch (and its Apache JsonPatchImpl) is capable of working with complex types as JsonValue mentions JsonObject and ValueType.OBJECT, but I don't know how to instruct Jackson to deserialise correctly

Thanks in advance for any suggestions or help!


Solution

  • I went through this by using the JSR-364 Implementation Json.createPatch:

    @PatchMapping("/settings")
    public ResponseEntity<SettingsResponse> patchDefaultSettingsJsonP3(@RequestBody String patchString, Locale locale) {
        try (JsonReader jsonReader = Json.createReader(new StringReader(patchString))) {
            JsonPatch patch = Json.createPatch(jsonReader.readArray());
            ...
        }
    }
    

    EDIT: I found wiser solution by registering the converter as a bean. Spring then takes care of the deserialisation internally

    @Component
    public class JsonPatchHttpMessageConverter extends AbstractHttpMessageConverter<JsonPatch> {
    
    public JsonPatchHttpMessageConverter() {
        super(MediaType.valueOf("application/json-patch+json"), MediaType.APPLICATION_JSON);
    }
    
    @Override
    protected boolean supports(Class<?> clazz) {
        return JsonPatch.class.isAssignableFrom(clazz);
    }
    
    @Override
    protected JsonPatch readInternal(Class<? extends JsonPatch> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        try (JsonReader reader = Json.createReader(inputMessage.getBody())) {
            return Json.createPatch(reader.readArray());
        } catch (Exception e) {
            throw new HttpMessageNotReadableException(e.getMessage(), inputMessage);
        }
    }
    
    @Override
    protected void writeInternal(JsonPatch jsonPatch, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        throw new NotImplementedException("The write Json patch is not implemented");
    }
    }