英文:
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("/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!
答案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("/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");
}
}
答案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);
原因是缺少之前创建的用于启用JsonPatch
的ObjectMapper
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
usageJsonPatchHttpMessageConverter
- 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("${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));
// 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;
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论