I'm working on Jackson configuration and I wonder if there is any option to deserialise different kinds of field patterns.
For example, I have an object:
class DeserializeIt {
String fieldOne;
String fieldOneAndHalf;
String fieldTwo;
String fieldThree;
String fieldFour;
//getters setters etc.
}
And I have below JSON payload:
{
"fieldOne" : "value1",
"field_ONE-and_Half": "value15",
"FIELD_TWO": "value2",
"FIELD_THREE" : "value3",
"field_four": "value4"
}
I would like to deserialize all these field names to camel case without an exception.
I tried to create my custom PropertyNamingStrategy but it goes from another direction: it does not convert delimitered fields to camel case, it tries to convert the objects fields and search for them in the parsed string.
And since I cannot pass a list of possible strings instead of one variation (fieldOne can become field-one, field_one, field-ONE etc.), this does not work.
Do you know what else could I configure for such a relaxed deserialization?
We need to extend com.fasterxml.jackson.databind.deser.BeanDeserializerModifier and com.fasterxml.jackson.databind.deser.BeanDeserializer which deserialises POJO classes. Below solution depends from version you are using because I copied some code from base class which is not ready for intercepting extra functionality. If you do not have any extra configuration for your POJO classes vanillaDeserialize method will be invoked and this one we will try to improve.
In other case you need to debug this deserialiser and updated other places if needed. Below solution uses version 2.9.8.
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
SimpleModule relaxedModule = new SimpleModule();
relaxedModule.setDeserializerModifier(new RelaxedBeanDeserializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(relaxedModule);
System.out.println(mapper.readValue(jsonFile, DeserializeIt.class));
}
}
class RelaxedBeanDeserializerModifier extends BeanDeserializerModifier {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
JsonDeserializer<?> base = super.modifyDeserializer(config, beanDesc, deserializer);
if (base instanceof BeanDeserializer) {
return new RelaxedBeanDeserializer((BeanDeserializer) base);
}
return base;
}
}
class RelaxedBeanDeserializer extends BeanDeserializer {
private Map<String, String> properties = new HashMap<>();
public RelaxedBeanDeserializer(BeanDeserializerBase src) {
super(src);
_beanProperties.forEach(property -> {
properties.put(property.getName().toLowerCase(), property.getName());
});
}
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// common case first
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
// 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
// what it is, including "expected behavior".
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
protected Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException {
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
// [databind#631]: Assign current value, to be accessible by custom serializers
p.setCurrentValue(bean);
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName();
do {
String relaxedName = getRelaxedName(propName);
String mappedName = properties.get(relaxedName);
defaultImplementation(p, ctxt, bean, mappedName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}
private void defaultImplementation(JsonParser p, DeserializationContext ctxt, Object bean, String propName) throws IOException {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
return;
}
handleUnknownVanilla(p, ctxt, bean, propName);
}
private String getRelaxedName(String name) {
return name.replaceAll("[_\\-]", "").toLowerCase();
}
}
Above code prints:
DeserializeIt{fieldOne='value1', fieldOneAndHalf='value15', fieldTwo='value2', fieldThree='value3', fieldFour='value4'}
See also:
Can Jackson check for duplicated properties in a case insensitive way?
From Jackson 2.9, you can provide multiple possible properties names for deserialization using the
#JsonAlias annotation. On your example, it would be like this:
class DeserializeIt {
#JsonAlias("fieldOne")
String fieldOne;
#JsonAlias("field_ONE-and_Half")
String fieldOneAndHalf;
#JsonAlias("FIELD_TWO")
String fieldTwo;
#JsonAlias("FIELD_THREE")
String fieldThree;
// and so on...
}
What worked for myself: I added an AOP component that renames all the fields of incoming object into the Camel case.
Related
I have a rest service that consume json from an Angular UI and also from other rest clients. The data based on a complex structure of entities ~50 that are stored in a database with ~50 tables. The problem are the optional OneToOne relations, because Angular send the optional objects as empty definitions like "car": {},. The spring data repository saves them as empty entries and I got a Json response like "car": {"id": 545234, "version": 0} back. I found no Jackson annotation to ignore empty objects, only empty or null properties.
The Employee Entity has the following form:
#Entity
public class Employee {
#Id
#GeneratedValue
private Long id;
#Version
private Long version;
private String name;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "car_id")
#JsonManagedReference
private Car car;
.
. Desk, getters and setters
.
}
and the other side of the OneToOne Reference
#Entity
public class Car{
#Id
#GeneratedValue
private Long id;
#Version
private Long version;
private String name;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "employee")
#JsonBackReference
private Employee employee;
.
. getters and setters
.
}
For example, I send this to my service as post operation
{
"name": "ACME",
.
.
.
"employee": {
"name": "worker 1",
"car": {},
"desk": {
floor: 3,
number: 4,
phone: 444
}
.
.
.
},
"addresses": [],
"building": {},
.
.
.
}
and I got as response the saved data
{
"id": 34534,
"version": 0,
"name": "ACME",
.
.
.
"employee": {
"id": 34535,
"version":0,
"name": "worker 1",
"car": {"id": 34536, "version": 0},
"desk": {
"id": 34538,
"version":0,
"floor": 3,
"number": 4,
"phone": 444
}
.
.
.
},
"addresses": [],
"building": {"id": 34539, "version": 0},
.
.
.
}
As seen in the response I got empty table rows with an id, a version, many null values and empty strings, because when I save (persist) the main deserialized company class, the other entity are also saved, because the are annotated as cascading.
I found many examples like Do not include empty object to Jackson , with a concrete pojo and a concrete deserializer that are working, but every entity needs his own Deserializer. This causes many work for the current entities and the new ones in the future (only the optional entities).
I tried the folowing, I write a BeanDeserializerModifier and try to wrap an own deserializer over the standard beandeserializer:
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public List<BeanPropertyDefinition> updateProperties(DeserializationConfig config,
BeanDescription beanDesc,
List<BeanPropertyDefinition> propDefs) {
logger.debug("update properties, beandesc: {}", beanDesc.getBeanClass().getSimpleName());
return super.updateProperties(config, beanDesc, propDefs);
}
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
logger.debug("modify deserializer {}",beanDesc.getBeanClass().getSimpleName());
// This fails:
// return new DeserializationWrapper(deserializer, beanDesc);
return deserializer; // This works, but it is the standard behavior
}
});
And here is the wrapper (and the mistake):
public class DeserializationWrapper extends JsonDeserializer<Object> {
private static final Logger logger = LoggerFactory.getLogger( DeserializationWrapper.class );
private final JsonDeserializer<?> deserializer;
private final BeanDescription beanDesc;
public DeserializationWrapper(JsonDeserializer<?> deserializer, BeanDescription beanDesc) {
this.deserializer = deserializer;
this.beanDesc = beanDesc;
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
logger.debug("deserialize in wrapper {} ",beanDesc.getBeanClass().getSimpleName());
final Object deserialized = deserializer.deserialize(p, ctxt);
ObjectCodec codec = p.getCodec();
JsonNode node = codec.readTree(p);
// some logig that not work
// here. The Idea is to detect with the json parser that the node is empty.
// If it is empty I will return null here and not the deserialized pojo
return deserialized;
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException {
logger.debug("deserializer - method 2");
intoValue = deserializer.deserialize(p, ctxt);
return intoValue;
}
#Override
public boolean isCachable() {
return deserializer.isCachable();
}
.
. I try to wrap the calls to the deserializer
.
The Deserialization Wrapper does not work and crash after the first call with an exception com.fasterxml.jackson.databind.exc.MismatchedInputException: No _valueDeserializer assigned at [Source: (PushbackInputStream); line: 2, column: 11] (through reference chain: ... Company["name"])
My question: is there a way to extend the behavior of the working standard deserializer in the way, that the deserializer detect while parsing, that the current jsonNode is empty and return null instead the empty class instance? Perhaps my Idea is wrong and there is a completely other solution?
Solving it on the Angular UI side is no option. We use Jackson 2.9.5.
Using BeanDeserializerModifier with custom deserialiser is a good idea. You need to improve only a deserialiser implementation. In your example problem is with these lines:
final Object deserialized = deserializer.deserialize(p, ctxt); //1.
ObjectCodec codec = p.getCodec(); //2.
JsonNode node = codec.readTree(p); //3.
Line 1. reads JSON Object. In lines 2. and 3. you want to create a JsonNode but empty JSON Object was already read in line 1.. Next two lines will try to read rest of payload as JsonNode.
Jackson by default uses BeanDeserializer to deserialise regular POJO classes. We can extend this class and provide our own implementation. In version 2.10.1 deserialise method looks like this:
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
// common case first
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
// 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
// what it is, including "expected behavior".
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
In most cases, where there is no special treatment needed vanillaDeserialize method will be invoked. Let's look on it:
private final Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException {
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
// [databind#631]: Assign current value, to be accessible by custom serializers
p.setCurrentValue(bean);
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName();
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}
As you can see, it does almost everything what we want, except it creates new object even for empty JSON Object. It checks whether field exists just after it creates new objects. One line too far. Unfortunately, this method is private and we can not override it. Let's copy it to our class and modify a little bit:
class EmptyObjectIsNullBeanDeserializer extends BeanDeserializer {
EmptyObjectIsNullBeanDeserializer(BeanDeserializerBase src) {
super(src);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt);
}
return super.deserialize(p, ctxt);
}
private Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
p.nextToken();
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
// [databind#631]: Assign current value, to be accessible by custom serializers
p.setCurrentValue(bean);
String propName = p.getCurrentName();
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null);
return bean;
}
return null;
}
}
You can register it like below:
class EmptyObjectIsNullBeanDeserializerModifier extends BeanDeserializerModifier {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (beanDesc.getBeanClass() == Car.class) { //TODO: change this condition
return new EmptyObjectIsNullBeanDeserializer((BeanDeserializerBase) deserializer);
}
return super.modifyDeserializer(config, beanDesc, deserializer);
}
}
Simple POC:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Data;
import java.io.File;
import java.io.IOException;
public class EmptyObjectIsNullApp {
public static void main(String[] args) throws IOException {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
SimpleModule emptyObjectIsNullModule = new SimpleModule();
emptyObjectIsNullModule.setDeserializerModifier(new EmptyObjectIsNullBeanDeserializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(emptyObjectIsNullModule);
Wrapper wrapper = mapper.readValue(jsonFile, Wrapper.class);
System.out.println(wrapper);
}
}
#Data
class Wrapper {
private Car car;
}
#Data
class Car {
private int id;
}
For "empty object" JSON payload:
{
"car": {}
}
Above code prints:
Wrapper(car=null)
For JSON payload with some fields:
{
"car": {
"id": 1
}
}
Above code prints:
Wrapper(car=Car(id=1))
With this data model...
TestClass.kt
data class TestClass (val bar: Optional<Double>?)
My goal is to deserialize the following json values as such:
{"foo": 3.5} --> foo = 3.5
{"foo": null} --> foo = Optional.empty() // This is currently my problem. foo is null and I can't seem to fix it
{} --> foo = null
I've seen the solution here and tried this, but the breakpoints in my "deserialize" method never seem to hit.
OptionalDeserializer.java
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import java.io.IOException;
import java.util.Optional;
public class OptionalDeserializer extends JsonDeserializer<Optional<?>> implements ContextualDeserializer {
private JavaType valueType;
#Override
public JsonDeserializer<?> createContextual(DeserializationContext context, BeanProperty property) {
this.valueType = property.getType().containedType(0);
return this;
}
#Override
public Optional<?> deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
final JsonNode node = parser.getCodec().readTree(parser);
return node.isNull()
? Optional.empty()
: Optional.of(context.readValue(parser, valueType));
}
}
TestDeserialization.kt
fun main(): {
val objectMapper = ObjectMapper().registerModule(KotlinModule())
val module = SimpleModule()
module.addDeserializer(Optional::class.java, OptionalDeserializer())
objectMapper.registerModule(module)
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
objectMapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true)
objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true)
objectMapper.nodeFactory = JsonNodeFactory.withExactBigDecimals(true)
val inputJson = """{"foo" : null}"""
val expectedObject = TestClass(foo = Optional.empty())
val actualObject = objectMapper.readValue(inputJson, TestClassBravo::class.java)
assertEquals(expectedObject, actualObject)
}
build.gradle (for version info)
compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
compile 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8'
There are two ways you can handle this.
It would be easier not to use a custom serializer, but rely on the jackson default Jdk8Module (see here)
If you want your custom Deserializer, you need to specify the null access pattern, else the deserializer will not be called for null values:
Code for 2:
// In OptionalDeserializer
#Override
public AccessPattern getNullAccessPattern() {
return AccessPattern.CONSTANT;
}
I have a REST controller that returns an Object. I want to set the PropertyNamingStrategy at the time of controller action response. For instance, based on who calls this controller, I wanna respond by sending a Camel case naming convention, and in another case return a Snake case naming style. I can use an object mapper in my controller action but that only returns a String type. I still want my Object type but with different naming convention. Any suggestions?
I'm using Springboot 2.x.
I figured out how to solve this problem. I extended the naming strategy like:
public class GetSnakeCaseSetCamelCaseNamingStrategy
extends PropertyNamingStrategy.PropertyNamingStrategyBase {
#Override
public String translate(String propertyName) {
return propertyName;
}
#Override
public String nameForGetterMethod(
MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return PropertyNamingStrategy.SNAKE_CASE.nameForGetterMethod(config, method, defaultName);
}
#Override
public String nameForSetterMethod(
MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return PropertyNamingStrategy.UPPER_CAMEL_CASE.nameForSetterMethod(config, method, defaultName);
}
}
Then annotated my Pojo with my own naming strategy and it worked!
If your rest controller returns a POJO and that POJO needs to be converted to JSON with different naming strategy eg SNAKE_CASE for a given type of user and CAMEL_CASE for other type of user based on your requirement, then you need to create your own HttpMessageConverter and register it with Spring.
Your Custom HttpMessageConverter looks like this:-
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.TypeUtils;
import java.io.IOException;
import java.lang.reflect.Type;
class CustomJackson2HttpMessageConverter
extends MappingJackson2HttpMessageConverter {
private static final ObjectMapper snakeCaseObjectMapper = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
ObjectMapper defaultObjectMapper = new ObjectMapper();
#Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
//Note:- Get the appropriate ObjectMapper and use them to write Object to Json string accordingly
ObjectMapper objectMapper = getObjectMapper(outputMessage);
JsonGenerator generator = objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);
Class<?> serializationView = null;
FilterProvider filters = null;
Object value = object;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
ObjectWriter objectWriter;
if (serializationView != null) {
objectWriter = objectMapper.writerWithView(serializationView);
} else if (filters != null) {
objectWriter = objectMapper.writer(filters);
} else {
objectWriter = objectMapper.writer();
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, object);
generator.flush();
} catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
} catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
}
ObjectMapper getObjectMapper(HttpOutputMessage outputMessage) {
HttpHeaders responseHeaders = outputMessage.getHeaders();
/** TODO:- Check in responseHeaders if it is a specific user demanding
* response with PropertyNamingStrategy as PropertyNamingStrategy.SNAKE_CASE
* For eg:- you can have a header with name- jsonNamingStrategy=SNAKE_CASE or any default value or empty
* Based on jsonNamingStrategy you can decide if user wants response in SnakeCase or default one
* Return appropriately based on your check. For now returning snakeCaseObjectMapper
**/
return snakeCaseObjectMapper;
}
}
And you need to register your CustomJackson2HttpMessageConverter with Spring as:-
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
#EnableWebMvc
#Configuration
class WebConfig implements WebMvcConfigurer {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
final MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter = new CustomJackson2HttpMessageConverter();
//Replace the default MappingJackson2HttpMessageConverter with the CustomJackson2HttpMessageConverter
converters.replaceAll(it -> it instanceof MappingJackson2HttpMessageConverter ? customJackson2HttpMessageConverter : it);
if (!converters.contains(customJackson2HttpMessageConverter)) {
converters.add(customJackson2HttpMessageConverter);
}
}
}
Similarly if you want to handle the scenario where you want to parse JSON with different naming strategy to POJO, then you can override method readInternal() of the CustomJackson2HttpMessageConverter.
I try to write custom jackson deserializer. I want "look" at one field and perform auto deserialization to class, see example below:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.mypackage.MyInterface;
import com.mypackage.MyFailure;
import com.mypackage.MySuccess;
import java.io.IOException;
public class MyDeserializer extends JsonDeserializer<MyInterface> {
#Override
public MyInterface deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectCodec codec = jp.getCodec();
JsonNode node = codec.readTree(jp);
if (node.has("custom_field")) {
return codec.treeToValue(node, MyFailure.class);
} else {
return codec.treeToValue(node, MySuccess.class);
}
}
}
Pojos:
public class MyFailure implements MyInterface {}
public class MySuccess implements MyInterface {}
#JsonDeserialize(using = MyDeserializer.class)
public interface MyInterface {}
And I got StackOverflowError. In understand that codec.treeToValue call same deserializer. Is there a way to use codec.treeToValue or ObjectMapper.readValue(String,Class<T>) inside custome deseralizer?
The immediate problem seems to be that the #JsonDeserialize(using=...) is being picked up for your implementations of MyInterface as well as MyInterface itself: hence the endless loop.
You can fix this my overriding the setting in each implementation:
#JsonDeserialize(using=JsonDeserializer.None.class)
public static class MySuccess implements MyInterface {
}
Or by using a module instead of an annotation to configure the deserialization (and removing the annotation from MyInterface):
mapper.registerModule(new SimpleModule() {{
addDeserializer(MyInterface.class, new MyDeserializer());
}});
On a side-note, you might also consider extending StdNodeBasedDeserializer to implement deserialization based on JsonNode. For example:
#Override
public MyInterface convert(JsonNode root, DeserializationContext ctxt) throws IOException {
java.lang.reflect.Type targetType;
if (root.has("custom_field")) {
targetType = MyFailure.class;
} else {
targetType = MySuccess.class;
}
JavaType jacksonType = ctxt.getTypeFactory().constructType(targetType);
JsonDeserializer<?> deserializer = ctxt.findRootValueDeserializer(jacksonType);
JsonParser nodeParser = root.traverse(ctxt.getParser().getCodec());
nodeParser.nextToken();
return (MyInterface) deserializer.deserialize(nodeParser, ctxt);
}
There are a bunch of improvements to make to this custom deserializer, especially regarding tracking the context of the deserialization etc., but this should provide the functionality you're asking for.
In order to use your own ObjectMapper inside a custom deserializer, you can use Jackson Mix-in Annotations (the DefaultJsonDeserializer interface) to dynamically remove the custom deserializer from the POJO classes, avoiding the StackOverflowError that would otherwise be thrown as a result of objectMapper.readValue(JsonParser, Class<T>).
public class MyDeserializer extends JsonDeserializer<MyInterface> {
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.addMixIn(MySuccess.class, DefaultJsonDeserializer.class);
objectMapper.addMixIn(MyFailure.class, DefaultJsonDeserializer.class);
}
#Override
public MyInterface deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
if (jp.getCodec().<JsonNode>readTree(jp).has("custom_field")) {
return objectMapper.readValue(jp, MyFailure.class);
} else {
return objectMapper.readValue(jp, MySuccess.class);
}
}
#JsonDeserialize
private interface DefaultJsonDeserializer {
// Reset default json deserializer
}
}
I find a solution to use object mapper inside custom deserialize
public class DummyDeserializer extends JsonDeserializer<Dummy> {
#Override
public Dummy deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
ObjectMapper om = new ObjectMapper();
om.addMixIn(NexusAccount.class, DefaultJsonDeserializer.class);
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
String serviceType = node.path("serviceType").asText();
switch (serviceType) {
case "Dummy1":
return om.treeToValue(node, Dumm1.class);
case "Dummy2":
return om.treeToValue(node, Dummy2.class);
case "Dummy3":
return om.treeToValue(node, Dummy3.class);
default:
throw new IllegalArgumentException("Unknown Dummy type");
}
}
#JsonDeserialize
private interface DefaultJsonDeserializer {
// Reset default json deserializer
}
}
This did the trick for me:
ctxt.readValue(node, MyFailure.class)
Using Jackson and jackson-dataformat-xml 2.4.4, I'm trying to deserialize a XML document where a collection annotated with #XmlWrapperElement may have zero elements, but where the XML contains whitespace (in my case a line break). Jackson throws a JsonMappingException on this content with the message “Can not deserialize instance of java.util.ArrayList out of VALUE_STRING token”. I cannot change the way the XML is produced.
Example:
static class Outer {
#XmlElementWrapper
List<Inner> inners;
}
static class Inner {
#XmlValue
String foo;
}
ObjectMapper mapper = new XmlMapper().registerModules(new JaxbAnnotationModule());
String xml = "<outer><inners>\n</inners></outer>";
Outer outer = mapper.readValue(xml, Outer.class);
The following workarounds do not work:
Enabling DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY: In this case Jackson wants to instantiate a bogus instance of Inner using the whitespace as content.
Creating setters for this field for both String and the collection type. In this case I get a JsonMappingException (“Conflicting setter definitions for property "inners"”).
In a similar Stackoverflow question it is suggested to downgrade Jackson to 2.2.3. This does not fix the problem for me.
Any suggestions?
Edit: I can work around this issue by wrapping the CollectionDeserializer and checking for a whitespace token. This looks however very fragile to me, e.g. I had to override another method to rewrap the object. I can post the workaround, but a cleaner approach would be better.
A workaround for this problem is to wrap the standard CollectionDeserializer to return an empty collection for tokens containing whitespace and register the new Deserializer. I put the code into a Module so it can be registered easily:
import java.io.IOException;
import java.util.Collection;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.CollectionType;
public class XmlWhitespaceModule extends SimpleModule {
private static class CustomizedCollectionDeserialiser extends CollectionDeserializer {
public CustomizedCollectionDeserialiser(CollectionDeserializer src) {
super(src);
}
private static final long serialVersionUID = 1L;
#SuppressWarnings("unchecked")
#Override
public Collection<Object> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
if (jp.getCurrentToken() == JsonToken.VALUE_STRING
&& jp.getText().matches("^[\\r\\n\\t ]+$")) {
return (Collection<Object>) _valueInstantiator.createUsingDefault(ctxt);
}
return super.deserialize(jp, ctxt);
}
#Override
public CollectionDeserializer createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException {
return new CustomizedCollectionDeserialiser(super.createContextual(ctxt, property));
}
}
private static final long serialVersionUID = 1L;
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyCollectionDeserializer(
DeserializationConfig config, CollectionType type,
BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (deserializer instanceof CollectionDeserializer) {
return new CustomizedCollectionDeserialiser(
(CollectionDeserializer) deserializer);
} else {
return super.modifyCollectionDeserializer(config, type, beanDesc,
deserializer);
}
}
});
}
}
After that you can add it to your ObjectMapper like this:
ObjectMapper mapper = new XmlMapper().registerModule(new XmlWhitespaceModule());