Restructure JSON before deserializing with Jackson - java

We have a service which currently consumes JSON. We want to slightly restructure this JSON (move one property one level up) but also implement graceful migration so that our service could process old structure as well as new structure. We're using Jackson for JSON deserialization.
How do we restructure JSON prior to deserialization with Jackson?
Here's a MCVE.
Assume our old JSON looks as follows:
{"reference": {"number" : "one", "startDate" : [2016, 11, 16], "serviceId" : "0815"}}
We want to move serviceId one level up:
{"reference": {"number" : "one", "startDate" : [2016, 11, 16]}, "serviceId" : "0815"}
This are the classes we want to deserialize from both old an new JSONs:
public final static class Container {
public final Reference reference;
public final String serviceId;
#JsonCreator
public Container(#JsonProperty("reference") Reference reference, #JsonProperty("serviceId") String serviceId) {
this.reference = reference;
this.serviceId = serviceId;
}
}
public final static class Reference {
public final String number;
public final LocalDate startDate;
#JsonCreator
public Reference(#JsonProperty("number") String number, #JsonProperty("startDate") LocalDate startDate) {
this.number = number;
this.startDate = startDate;
}
}
We only want serviceId in Container, not in both classes.
What I've got working is the following deserializer:
public static class ServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {
private final ObjectMapper objectMapper;
{
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
#Override
public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectNode node = p.readValueAsTree();
migrate(node);
return objectMapper.treeToValue(node, Container.class);
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
This deserializer first retrieves the tree, manipulates it and then deserializers it using an own instance of ObjectMapper. It works but we really dislike the fact that we have another instance of ObjectMapper here. If we don't create it and somehow use the system-wide instance of ObjectMapper we get an infinite cycle because when we try to call objectMapper.treeToValue, our deserializer gets called recursively. So this works (with an own instance of ObjectMapper) but it is not an optimal solution.
Another method I've tried was using a BeanDeserializerModifier and a own JsonDeserializer which "wraps" the default serializer:
public static class ServiceIdMigrationBeanDeserializerModifier extends BeanDeserializerModifier {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> defaultDeserializer) {
if (beanDesc.getBeanClass() == Container.class) {
return new ModifiedServiceIdMigratingContainerDeserializer((JsonDeserializer<Container>) defaultDeserializer);
} else {
return defaultDeserializer;
}
}
}
public static class ModifiedServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {
private final JsonDeserializer<Container> defaultDeserializer;
public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<Container> defaultDeserializer) {
this.defaultDeserializer = defaultDeserializer;
}
#Override
public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectNode node = p.readValueAsTree();
migrate(node);
return defaultDeserializer.deserialize(new TreeTraversingParser(node, p.getCodec()), ctxt);
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
"Wrapping" a default deserializer seems to be a better approach, but this fails with an NPE:
java.lang.NullPointerException
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:157)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:150)
at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:235)
at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:1)
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1623)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1217)
at ...
The whole MCVE code is in the following PasteBin. It is a single-class all-containing test case which demonstrates both approaches. The migratesViaDeserializerModifierAndUnmarshalsServiceId fails.
So this leaves me with a question:
How do we restructure JSON prior to deserialization with Jackson?

In the best traditions, right after posting the question, I've managed to solve this.
Two things:
I had to do newJsonParser.nextToken(); to avoid NPE.
Extend DelegatingDeserializer
Here's a working DelegatingDeserializer:
public static class ModifiedServiceIdMigratingContainerDeserializer
extends DelegatingDeserializer {
public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<?> defaultDeserializer) {
super(defaultDeserializer);
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new ModifiedServiceIdMigratingContainerDeserializer(newDelegatee);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return super.deserialize(restructure(p), ctxt);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException,
JsonProcessingException {
return super.deserialize(restructure(p), ctxt, intoValue);
}
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException, JsonProcessingException {
return super.deserializeWithType(restructure(jp), ctxt, typeDeserializer);
}
public JsonParser restructure(JsonParser p) throws IOException, JsonParseException {
final ObjectNode node = p.readValueAsTree();
migrate(node);
final TreeTraversingParser newJsonParser = new TreeTraversingParser(node, p.getCodec());
newJsonParser.nextToken();
return newJsonParser;
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}

Related

jackson objectmapper does not use custom [de]serializer when calling writeValueAsString

My overall goal is to have "null" lists deserialized and serialized as empty lists.
So creating a class with the variable List foo with a null value, will finally be set as the empty list.
The same when writing the object as json. If List foo is null, I need it written as {"foo": []}.
But, no matter what I try, my objectmapper does not recognize any of the things I am doing.
Relevant class
#ApiModel(description = "A rule project.")
#JsonPropertyOrder({
Project.JSON_PROPERTY_NAME
Project.JSON_PROPERTY_CONSTANTS
})
#javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJerseyServerCodegen", date = "2022-05-17T09:06:04.105050+02:00[Europe/Berlin]")
public class Project {
public static final String JSON_PROPERTY_NAME = "name";
#JsonProperty(JSON_PROPERTY_NAME)
private String name;
public Project name(String name) {
this.name = name;
return this;
}
#JsonDeserialize(contentUsing = CountryDeserializer.class)
#JsonSerialize(contentUsing = ListSerializer.class)
private List<Constant> constants = null;
#JsonDeserialize(contentUsing = CountryDeserializer.class)
#JsonSerialize(contentUsing = ListSerializer.class)
public List<Constant> getConstants() {
return constants;
}
#JsonDeserialize(contentUsing = CountryDeserializer.class)
#JsonSerialize(contentUsing = ListSerializer.class)
public void setConstants(List<Constant> constants) {
this.constants = constants;
}
[De]serializers
public class CountryDeserializer extends JsonDeserializer<Constant> {
#Override
public Constant deserialize(JsonParser p, DeserializationContext ctxt, Constant intoValue) throws IOException {
// TODO Auto-generated method stub
return super.deserialize(p, ctxt, intoValue);
}
#Override
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException {
// TODO Auto-generated method stub
return super.deserializeWithType(p, ctxt, typeDeserializer);
}
#Override
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer,
Constant intoValue) throws IOException {
// TODO Auto-generated method stub
return super.deserializeWithType(p, ctxt, typeDeserializer, intoValue);
}
#Override
public Constant deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException {
return null;
}
}
public class ListSerializer extends JsonSerializer<List> {
#Override
public void serializeWithType(List value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer)
throws IOException {
// TODO Auto-generated method stub
super.serializeWithType(value, gen, serializers, typeSer);
}
#Override
public void serialize(List value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
if (null == value) {
provider.defaultSerializeValue(new ArrayList<Object>(), jgen);
} else {
provider.defaultSerializeValue(value, jgen);
}
}
}
Things I tried:
As you can see I tried to annotate the fields, getters and setters to use my custom serializers, but the objectmapper never uses them (tried by setting breakpoints).
I also tried to configure the object mapper itself with various options:
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY))
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
Or by registering the [de]serializers as SimpleModule
SimpleModule module = new SimpleModule();
module.addDeserializer(List.class, new ListDeserializer());
objectMapper.registerModule(module);
But nothing works, it's just weird.
I am using jackson-databind version 2.12.3
Any idea what is going on here?

How to create custom Generic deserializer in Jackson?

I am looking to create a custom Deserializer in Jackson for set of Enum class. Since the behaviour of custom deserializer would be same for all enum. I want to make common Deserializer for all my enum class.
I tried making generic custom deserialize as follow:
class MyEnumDeserialize<T> extends JsonDeserializer<T> {
private Class beanClass;
public MyEnumDeserialize() {
}
public MyEnumDeserialize(Class beanClass) {
this.beanClass = beanClass;
}
#Override
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
TreeNode node = jsonParser.getCodec().readTree(jsonParser);
T type = null;
try{
if(node.get("attr") != null){
// I don't know how to call ENUM static method here as I don't have context information here
if (type != null) {
return type;
}
}
}catch(Exception e){
type = null;
}
return null;
}
}
The problem is I want to call Enum static method inside the deserializer but unable to do so since I don't have any class/enum context information available.
Could you please help me know how could I achieve it.
I somehow managed to make following solution worked:
I created a module and modify EnumDeserializer as follow:
module.setDeserializerModifier(new BeanDeserializerModifier()
{
#Override public JsonDeserializer<?> modifyEnumDeserializer(DeserializationConfig config,
JavaType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer)
{
if (beanDesc.getBeanClass().isEnum()) {
return new MyEnumDeserialize<>(beanDesc.getBeanClass());
}
return deserializer;
}
});
class MyEnumDeserialize<T extends Enum> extends JsonDeserializer<T> {
private Class beanClass;
public MyEnumDeserialize() {
}
public MyEnumDeserialize(Class beanClass) {
this.beanClass = beanClass;
}
#Override
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
TreeNode node = jsonParser.getCodec().readTree(jsonParser);
T type = null;
try{
if(node.isValueNode()){
ValueNode valueNode = (ValueNode)node;
Method method = this.beanClass.getDeclaredMethod("get",short.class);
type = (T)method.invoke(null, Short.parseShort(valueNode.asText()));
if (type != null) {
return type;
}
}
}catch(Exception e){
type = null;
}
return type;
}
}
Though now, I am unable to provide Strict type checking ( creating instance with "<>").

Java Jackson serializer including FQCN

I'm trying to create a generic Jackon polymorphic serializer that is able to serialize and deserialize to and from JSON with this format including the fqcn of the class of the object:
{
"fqcn": "full qualified class name of the object",
"data": "serialized object"
}
This wrapper should be applied to any object, so for example this will be the JSON representation of a HashMap> object:
{
"fqcn": "java.util.HashMap",
"data": {
"key1": {
"fqcn": "java.util.ArrayList",
"data": [
{
"fqcn": "java.lang.String",
"data": "value1"
},
{
"fqcn": "java.lang.String",
"data": "value2"
}
]
},
"key2": {
...
}
}
}
I could use a MixIn annotation all objects with #JsonTypeInfo
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_OBJECT)
public interface ObjMixin {
}
---
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Object.class, ObjMixin.class);
However, the format does not match with the required format: {"fqcn": ..., "data": ...}
I've also tried to register a StdConverter to convert any objects to a wrapper like this:
public class ObjectWrapper {
private String fqcn;
private Object data;
public ObjectWrapper(Object obj) {
this.fqcn = obj.getClass.getCanonicalName();
this.data = obj;
}
}
However it is not possible to create a StdDelegatingSerializer for Object.class.
With a custom StdSerializer like the following I am getting StackOverflowError:
#Override
public void serialize(Object obj, JsonGenerator jsonGen, SerializerProvider serializerProvider) throws IOException {
jsonGen.writeStartObject();
jsonGen.writeStringField("fqcn", obj.getClass().getCanonicalName());
jsonGen.writeFieldName("data");
if (obj instanceof Iterable) {
jsonGen.writeStartArray();
// Recursive serialization of all elements in the iterable
jsonGen.writeEndArray();
} else if (obj instanceof Map) {
jsonGen.writeStartObject();
// Recursive serialization of all elements in the map
jsonGen.writeEndObject();
} else {
// Infinite recursion here because I'm defining this serializer for Object.class
serializerProvider.defaultSerializeValue(obj, jsonGen);
}
}
Does anyone know any other solution to be able to achieve this?
You could use a custom serializer and custom serializer provider to wrap every object you want to serialize into this wrapper object (EDIT: that did not work recusrively, updated the code to not use the wrapper object but write the fields instead):
public class FQCNTest {
#Test
public void doTest() throws JsonProcessingException {
final ObjectMapper om = getObjectMapper();
final Object obj = getTestObject();
final String json = om.writeValueAsString(obj);
System.out.println(json); // {"fqcn":"java.util.HashMap","data":{"k":{"fqcn":"java.lang.String","data":"v"}}}
final Object obj2 = getTestValue();
final String json2 = om.writeValueAsString(obj2);
System.out.println(json2); // {"fcqn":"java.lang.String","data":"hello"}
final Object obj3 = null;
final String json3 = om.writeValueAsString(obj3);
System.out.println(json3); // null
}
private ObjectMapper getObjectMapper() {
final ObjectMapper mapper = new ObjectMapper();
final SerializerProvider sp = mapper.getSerializerProviderInstance();
mapper.setSerializerProvider(new CustomSerializerProvider(sp, mapper.getSerializerFactory()));
return mapper;
}
private Object getTestObject() {
final HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("k", "v");
return hashMap;
}
private Object getTestValue() {
return "hello";
}
}
class CustomSerializerProvider extends DefaultSerializerProvider {
private final SerializerProvider defaultInstance;
protected CustomSerializerProvider(final SerializerProvider defaultInstance, final SerializerFactory f) {
super(defaultInstance, defaultInstance.getConfig(), f);
this.defaultInstance = defaultInstance;
}
#Override
public WritableObjectId findObjectId(final Object forPojo, final ObjectIdGenerator<?> generatorType) {
return defaultInstance.findObjectId(forPojo, generatorType);
}
#Override
public JsonSerializer<Object> serializerInstance(final Annotated annotated, final Object serDef) throws JsonMappingException {
return new CustomSerializer();
}
#Override
public Object includeFilterInstance(final BeanPropertyDefinition forProperty, final Class<?> filterClass) {
try {
return defaultInstance.includeFilterInstance(forProperty, filterClass);
} catch (final JsonMappingException e) {
throw new RuntimeException(e);
}
}
#Override
public boolean includeFilterSuppressNulls(final Object filter) throws JsonMappingException {
return defaultInstance.includeFilterSuppressNulls(filter);
}
#Override
public DefaultSerializerProvider createInstance(final SerializationConfig config, final SerializerFactory jsf) {
return this;
}
#Override
public void serializeValue(final JsonGenerator gen, final Object value) throws IOException {
new CustomSerializer().serialize(value, gen, this);
}
#Override
public void serializeValue(final JsonGenerator gen, final Object value, final JavaType rootType) throws IOException {
super.serializeValue(gen, value, rootType);
}
#Override
public void serializeValue(final JsonGenerator gen, final Object value, final JavaType rootType, final JsonSerializer<Object> ser) throws IOException {
super.serializeValue(gen, value, rootType, ser);
}
}
class CustomSerializer extends StdSerializer<Object> {
protected CustomSerializer() {
super(Object.class);
}
#Override
public void serialize(final Object value, final JsonGenerator gen, final SerializerProvider provider) throws IOException {
if (value == null) {
provider.defaultSerializeValue(value, gen);
return;
}
final Class<?> clazz = value.getClass();
final JsonSerializer<Object> serForClazz = provider.findValueSerializer(clazz);
gen.writeStartObject();
gen.writeStringField("fqcn", clazz.getCanonicalName());
gen.writeFieldName("data");
if (value instanceof Iterable) {
gen.writeStartArray();
for (final Object e : ((Iterable<?>) value)) {
final JsonSerializer<Object> ser = new CustomSerializer();
ser.serialize(e, gen, provider);
}
gen.writeEndArray();
} else if (value instanceof Map) {
gen.writeStartObject();
// Recursive serialization of all elements in the map
for (final Map.Entry<?, ?> e : ((Map<?, ?>) value).entrySet()) {
final String key = e.getKey().toString(); // need to handle keys better
final Object mapValue = e.getValue();
gen.writeFieldName(key);
final JsonSerializer<Object> ser = new CustomSerializer();
ser.serialize(mapValue, gen, provider);
}
gen.writeEndObject();
} else {
serForClazz.serialize(value, gen, provider);
}
gen.writeEndObject();
}
}
Note: this code may contain too much stuff that is not necessary, I just took it far enough to make it work for the specific example. (and did not test deserialization, that may be a totally different thing)

Serialize List to xml with Jackson without Annotation?

I'm looking for a way to (de-)serialize a List of items without using Annotations in Jackson. Is this possible? What I'm doing up to now is trying to replace the <item>-tag with a tag telling about the item's class, but no avail. And even if this worked, I'm not sure whether Jackson would offer a way to process this tag information.
To give a better of what I'm aiming at, here's a sample:
public class JacksonTest {
private static class ListElement {
private boolean value;
// getters, setters, constructors omitted
}
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true));
final ObjectMapper mapper = new XmlMapper();
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final String listString = mapper.writerFor(listJavaType).writeValueAsString(existing);
System.out.println(listString);
// "<List><item><value>true</value></item></List>"
}
}
So, the result is <List><item><value>true</value></item></List>, while I want the <item>-tag to be replaced with the (qualified) class name or offering a type-attribute.
Of course, even this would not help if there's no way in Jackson to process this class name.
Do I have reached a dead end here or is there a way to go?
You can define your own JsonSerializer (also used for XML) and add it to a JacksonXmlModule.
ToXmlGenerator has a setNextName function that allows you to override the default item name
private class MyListSerializer extends JsonSerializer<List> {
#Override
public void serialize(List list, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
for (Object obj : list) {
if (jsonGenerator instanceof ToXmlGenerator) {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
String className = obj.getClass().getSimpleName();
xmlGenerator.setNextName(new QName(className));
}
jsonGenerator.writeObject(obj);
// this is overridden at the next iteration
// and ignored at the last
jsonGenerator.writeFieldName("dummy");
}
}
#Override
public Class<List> handledType() {
return List.class;
}
}
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true));
JacksonXmlModule module = new JacksonXmlModule();
module.addSerializer(new MyListSerializer());
final ObjectMapper mapper = new XmlMapper(module);
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final ObjectWriter writer = mapper.writerFor(listJavaType);
final String listString = writer.writeValueAsString(existing);
System.out.println(listString);
// "<List><ListElement><value>true</value></ListElement></List>"
}
Okay, after some tinkering and debugging with Evertude's proposal I've figured out a solution. I'm not really happy with the serialization part and honestly I don't know why I was supposed to do it this way. When debugging I've noticed that XmlGenerator::setNextName is required to be called once but does not have any effect on the next call, so I had to implement a switch there and set the field name for the next item in the loop directly.
I'ld be glad if somebody has an idea what I'm doing wrong, but at least my attempt is working for now:
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true), new ListElement(false));
JacksonXmlModule module = new JacksonXmlModule();
module.addSerializer(new MyListSerializer());
final ObjectMapper mapper = new XmlMapper(module);
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final ObjectWriter writer = mapper.writerFor(listJavaType);
final String listString = writer.writeValueAsString(existing);
module.addDeserializer(List.class, new MyListDeserializer());
List<ListElement> deserialized = mapper.readValue(listString, List.class);
assertEquals(existing, deserialized); // provided there're proper hash() and equals() methods
}
private class MyListSerializer extends JsonSerializer<List> {
#Override
public void serialize(List list, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
boolean done = false;
for (Object obj : list) {
if (jsonGenerator instanceof ToXmlGenerator) {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
String className = obj.getClass().getSimpleName();
// weird switch
if (!done) xmlGenerator.setNextName(new QName(className));
else jsonGenerator.writeFieldName(className);
done = true;
}
jsonGenerator.writeObject(obj);
}
}
#Override
public Class<List> handledType() {
return List.class;
}
}
private class MyListDeserializer extends JsonDeserializer<List> {
#Override
public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
List<Object> items = new ArrayList<>();
JsonToken nextToken;
while ((nextToken = p.nextToken()) != JsonToken.END_OBJECT) {
String currentName = p.currentName();
try {
String className = "my.test.project.JacksonCustomSerializer$" + currentName;
Class<?> loadClass = getClass().getClassLoader().loadClass(className);
p.nextToken();
items.add(p.readValueAs(loadClass));
} catch (ClassNotFoundException e) {
// some handling
}
}
return items;
}
#Override
public Class<List> handledType() {
return List.class;
}
}

How to delegate to default deserialization in custom deserializer in Jackson?

Suppose I am writing custom serialization for some class, but would like to process one of its field with default methods.
How to do that?
While serializing we have JsonGenerator#writeObjectField().
But what is corresponding method for deserialization?
Regard the code below:
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.Objects;
public class TryDelegate {
public static class MyOuterClassSerializer extends JsonSerializer<MyOuterClass> {
#Override
public void serialize(MyOuterClass value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
gen.writeStartObject();
gen.writeObjectField("inner", value.getInner());
gen.writeEndObject();
}
}
public static class MyOuterClassDeserializer extends JsonDeserializer<MyOuterClass> {
#Override
public MyOuterClass deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
MyOuterClass ans = new MyOuterClass();
JsonToken token;
token = p.getCurrentToken();
if( token != JsonToken.START_OBJECT ) {
throw new JsonParseException("Start object expected", p.getCurrentLocation());
}
if( !"inner".equals(p.nextFieldName() ) ) {
throw new JsonParseException("'inner; field expected", p.getCurrentLocation());
}
MyInnerClass inner = null;// how to desrialize inner from here with default processing???
ans.setInner(inner);
token = p.nextToken();
if( token != JsonToken.END_OBJECT ) {
throw new JsonParseException("End object expected", p.getCurrentLocation());
}
return ans;
}
}
public static class MyInnerClass {
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
#Override
public String toString() {
return "{\"value\":" + value + "}";
}
}
#JsonDeserialize(using = MyOuterClassDeserializer.class)
#JsonSerialize(using = MyOuterClassSerializer.class)
public static class MyOuterClass {
private MyInnerClass inner;
public MyInnerClass getInner() {
return inner;
}
public void setInner(MyInnerClass inner) {
this.inner = inner;
}
#Override
public String toString() {
return "{\"inner\":" + Objects.toString(inner) + "}";
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String string;
MyInnerClass inner = new MyInnerClass();
inner.setValue(12);
MyOuterClass outer = new MyOuterClass();
outer.setInner(inner);
string = mapper.writeValueAsString(outer);
System.out.println(string);
MyOuterClass outer2 = mapper.readValue(string, MyOuterClass.class);
System.out.println(outer2); // inner was not deserialized
}
}
How to implement MyOuterDeserializer?
The DeserializationContext offers these tools.
After checking the field name for "inner", move to the next token, the beginning of the JSON object and use the DeserializationContext to deserialize the JSON object into a MyInnerClass object.
if (!"inner".equals(p.nextFieldName())) {
throw new JsonParseException("'inner; field expected", p.getCurrentLocation());
}
p.nextToken(); // consumes the field name token
MyInnerClass inner = ctxt.readValue(p, MyInnerClass.class);
The javadoc states
Convenience method that may be used by composite or container
deserializers, for reading one-off values contained (for sequences, it
is more efficient to actually fetch deserializer once for the whole
collection).
Careful while using the DeserializationContext. Don't try to recursively deserialize types for which you have have registered custom deserializers.

Categories