Jackson用对象值反序列化JsonPatch。

huangapple go评论78阅读模式
英文:

Jackson deserialise JsonPatch with object value

问题

我在我的Spring项目的PATCH端点中使用了JsonPatch(JSR-374),实现来自Apache org.apache.johnzon:johnzon-core:1.2.4:

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

控制器:

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

使用简单原子值的json请求:

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

JsonPatch实例由Jackson正确反序列化。

但是对于复杂值类型(对象):

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

抛出异常:

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 无法构造javax.json.JsonPatch的实例(没有构造函数或默认构造函数),抽象类型需要映射到具体类型,具有自定义反序列化器,或包含附加类型信息。位置:[来源:(PushbackInputStream);行:1,列:1]

我认为JsonPatch(及其Apache JsonPatchImpl)能够处理复杂类型,因为JsonValue中提到了JsonObject和ValueType.OBJECT,但我不知道如何指示Jackson进行正确的反序列化。

提前感谢您的任何建议或帮助!

英文:

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(&quot;/settings&quot;)
public ResponseEntity&lt;SettingsResponse&gt; patchSettings(@RequestBody JsonPatch patchDocument, Locale locale) {...}

With json request of a simple atomic values

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

JsonPatch instance is deserialised correctly by Jackson

But with complex value type (object):

[
  { &quot;op&quot;: &quot;replace&quot;, &quot;path&quot;: &quot;/currency&quot;, &quot;value&quot;: {&quot;code&quot;: &quot;USD&quot;, &quot;label&quot;: &quot;US Dollar&quot;} },
  { &quot;op&quot;: &quot;test&quot;, &quot;path&quot;: &quot;/version&quot;, &quot;value&quot;: 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!

答案1

得分: 2

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 deserialization 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");
}
}
英文:

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

@PatchMapping(&quot;/settings&quot;)
public ResponseEntity&lt;SettingsResponse&gt; 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&lt;JsonPatch&gt; {

public JsonPatchHttpMessageConverter() {
    super(MediaType.valueOf(&quot;application/json-patch+json&quot;), MediaType.APPLICATION_JSON);
}

@Override
protected boolean supports(Class&lt;?&gt; clazz) {
    return JsonPatch.class.isAssignableFrom(clazz);
}

@Override
protected JsonPatch readInternal(Class&lt;? extends JsonPatch&gt; 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(&quot;The write Json patch is not implemented&quot;);
}
}

答案2

得分: 1

除了由Tomáš Mika提到的内容,我还要添加我的评论/用例。

前提条件

  • 使用Spring-Boot + Lombok + Jackson + JsonPatch
  • JsonPatchHttpMessageConverter - 如上所述2

在某个时间点,经过同事的cleanup提交后,所有的PATCH API都停止工作,抛出InvalidDefinitionException错误,错误消息如下:

Cannot construct instance of `javax.json.JsonValue` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information\n at [Source: UNKNOWN; byte offset: #UNKNOWN]

经过一段时间的调查后发现:

  • 异常是由ObjectMapper反序列化器引发的,具体是在方法中:

    protected Object _convert(Object fromValue, JavaType toValueType)

#4388

result = deser.deserialize(p, ctxt);

原因是缺少之前创建的用于启用JsonPatchObjectMapper bean,后来在某次cleanup的范围内错误地删除了它:

@Bean
public ObjectMapper objectMapper() {
    return new ObjectMapper()
            .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .findAndRegisterModules();
}

ObjectMapper bean已被显式定义,除了广泛的配置外:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String dateFormatPattern;

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.forEach(converter -> {
        if (converter instanceof MappingJackson2HttpMessageConverter) {
            MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter;
            ObjectMapper objectMapper = jacksonConverter.getObjectMapper();
            jacksonConverter.setPrettyPrint(true);
            configureObjectMapper(objectMapper);
        }
    });
}

private void configureObjectMapper(ObjectMapper mapper) {
    mapper.setTimeZone(TimeZone.getTimeZone("UTC"));
    mapper.setDateFormat(new SimpleDateFormat(dateFormatPattern));

    // ... 其他配置 ...

    mapper.registerModule(hibernate5Module());
    mapper.registerModule(new JavaTimeModule());
}

    @Bean
public Module hibernate5Module() {
    Hibernate5Module hibernate5Module = new Hibernate5Module();
    hibernate5Module.enable(Hibernate5Module.Feature.REPLACE_PERSISTENT_COLLECTIONS);
    return hibernate5Module;
  }
}
英文:

In addition to the mentioned above by Tomáš Mika I would add my comment / use-case.

Pre-conditions

  • Spring-Boot + Lombok + Jackson + JsonPatch usage
  • JsonPatchHttpMessageConverter - as defined above

At some point after colleagues cleanup commit all PATCH APIs have stopped working, by throwing the InvalidDefinitionException with error message:

Cannot construct instance of `javax.json.JsonValue` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information\n at [Source: UNKNOWN; byte offset: #UNKNOWN]

After some time & investigation it was found that:

  • Exception has thrown by ObjectMapper deserializer, specifically in method:

    protected Object _convert(Object fromValue, JavaType toValueType)

line #4388:

result = deser.deserialize(p, ctxt);

The reason was missing ObjectMapper bean, previously created to enable JsonPatch and lately removed by mistake in scope of some cleanup:

@Bean
public ObjectMapper objectMapper() {
    return new ObjectMapper()
            .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .findAndRegisterModules();
}

This ObjectMapper bean has been explicitly defined, in addition to the extensive configuration:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

@Value(&quot;${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}&quot;)
private String dateFormatPattern;

@Override
public void extendMessageConverters(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters) {
    converters.forEach(converter -&gt; {
        if (converter instanceof MappingJackson2HttpMessageConverter) {
            MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter;
            ObjectMapper objectMapper = jacksonConverter.getObjectMapper();
            jacksonConverter.setPrettyPrint(true);
            configureObjectMapper(objectMapper);
        }
    });
}

private void configureObjectMapper(ObjectMapper mapper) {
    mapper.setTimeZone(TimeZone.getTimeZone(&quot;UTC&quot;));
    mapper.setDateFormat(new SimpleDateFormat(dateFormatPattern));

    // https://programming.vip/docs/61b9674a30f8c.html
    mapper.setVisibility(
            mapper.getSerializationConfig().getDefaultVisibilityChecker()
                    .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
                    .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
                    .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
                    .withCreatorVisibility(JsonAutoDetect.Visibility.NONE)
    );

    // Serializers
    mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
    mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_EMPTY);
    mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    mapper.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
    mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    mapper.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
    mapper.disable(SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS);

    // Deserializers
    mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    mapper.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE);
    mapper.disable(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS);
    mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
    mapper.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);

    mapper.registerModule(hibernate5Module());
    mapper.registerModule(new JavaTimeModule());
}

    @Bean
public Module hibernate5Module() {
    Hibernate5Module hibernate5Module = new Hibernate5Module();
    hibernate5Module.enable(Hibernate5Module.Feature.REPLACE_PERSISTENT_COLLECTIONS);
    return hibernate5Module;
  }
}

huangapple
  • 本文由 发表于 2020年4月9日 14:57:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/61115545.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定