I am looking at different ways to workaround the fact that @JsonCreator does
not
"inherit" in the class hierarchy. I have tried different things, but have
failed to make it work without an ugly hack. The best I've come up with is
to use the Module.modifyDeserializer() method to modify or replace the new
deserializer. I need to replace the _propertyBasedCreator of this instance
but cannot find a way other than reflection surgery. See test code below.
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
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.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.impl.BeanPropertyMap;
import com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class Test_JsonCreator_Via_Deserializer {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include =
JsonTypeInfo.As.PROPERTY, property = "@class", visible = true)
@JsonIgnoreProperties({ "@class" })
public static class TestDomain {
private transient Object constructorAttr;
public String fieldAttr;
public TestDomain(Object consAttr) {
this.constructorAttr = consAttr;
}
@Override public String toString() {
return getClass().getName() + " " + constructorAttr + " " + fieldAttr;
}
}
public static class TestDomainSub extends TestDomain {
public TestDomainSub(Object consAttr) {
super(consAttr);
}
}
public static class TestModule extends SimpleModule {
public TestModule() {
super("TestDomain", new Version(1, 0, 0, null, null, null));
}
@Override public void setupModule(SetupContext context) {
// addValueInstantiator(TestDomain.class, new
ValueInstantiatorClassTestDomain(TestDomain,class));
// addValueInstantiator(TestDomainSub.class, new
ValueInstantiatorClassTestDomain(TestDomainSub,class));;
// ... and many many more ... this is manual and brittle ... no thanks
setDeserializerModifier(new BeanDeserializerModifier() {
@Override public JsonDeserializer<?>
modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
if (TestDomain.class.isAssignableFrom(beanDesc.getBeanClass())) {
// would have been nice to add ValueInstantiator here but it is too late
// _valueInstantiators = new SimpleValueInstantiators();
// _valueInstantiators =
_valueInstantiators.addValueInstantiator(beanDesc.getBeanClass(), new
MyValueInstantiator(beanDesc.getBeanClass()));
// context.addValueInstantiators(_valueInstantiators); //already moved on
try {
ValueInstantiator newValueInstantiator = new
MyValueInstantiator(beanDesc.getBeanClass());
// This is the ugly hack that works, but ugh
PropertyBasedCreator pbc = PropertyBasedCreator.construct(null,
newValueInstantiator, new SettableBeanProperty[0],
BeanPropertyMap.construct(new ArrayList(0), false, new HashMap()));
Field fields =
BeanDeserializerBase.class.getDeclaredField("_propertyBasedCreator");
makeFieldAccessible(fields);
fields.set(deserializer, pbc);
} catch (Exception e) {
System.err.println(e);
}
}
return deserializer;
}
});
super.setupModule(context);
}
static void makeFieldAccessible(Field field) {
try {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class MyValueInstantiator extends ValueInstantiator {
Class klass;
public MyValueInstantiator(Class klass) {
this.klass = klass;
}
@Override public Object createFromObjectWith(DeserializationContext ctxt,
Object[] args) throws IOException {
try {
Constructor constructor = klass.getConstructor(new Class[] { Object.class
});
Object externalInjected = ctxt.findInjectableValue("constructorAttr", null,
null);
return constructor.newInstance(new Object[] { externalInjected });
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new TestModule());
Object externalInjected = "constructor-field-value";
InjectableValues.Std injectableValues = new InjectableValues.Std();
injectableValues.addValue("constructorAttr", externalInjected);
objectMapper.setInjectableValues(injectableValues);
TestDomain td = new TestDomain(externalInjected);
td.fieldAttr = "field-attr-1";
String json = objectMapper.writeValueAsString(td);
System.out.println(json);
//
{"@class":"dataswarm.service.channel.input.Test_JsonCreator_Via_Deserializer$TestDomain","fieldAttr":"field-attr-1"}
TestDomain td2 = objectMapper.readValue(json, TestDomain.class);
System.out.println(td2);
//
dataswarm.service.channel.input.Test_JsonCreator_Via_Deserializer$TestDomain
constructor-field-value field-attr-1
TestDomainSub sub = new TestDomainSub(externalInjected);
sub.fieldAttr = "field-attr-2";
json = objectMapper.writeValueAsString(sub);
System.out.println(json);
//{"@class":"dataswarm.service.channel.input.Test_JsonCreator_Via_Deserializer$TestDomainSub","fieldAttr":"field-attr-2"}
TestDomain sub2 = objectMapper.readValue(json, TestDomain.class);
System.out.println(sub2);
//
dataswarm.service.channel.input.Test_JsonCreator_Via_Deserializer$TestDomainSub
constructor-field-value field-attr-2
}
Another way I tried was to create a custom Deserializer that would read the
"@class" attribute and handle instantiation. But I could never figure out
how to make a StdDeserializer handle instantiation then delegate everything
else to the "normal" deserialization. Something like the code below:
// unused. this did not work
static class OverrideInstantiationDeserializer<T extends TestDomain>
extends StdDeserializer<T> {
private JsonDeserializer<T> remainderDeserialization;
public OverrideInstantiationDeserializer(Class<T> beanClass,
JsonDeserializer<T> deserializer) {
super(beanClass);
this.remainderDeserialization = deserializer;
}
@Override public boolean isCachable() {
return true;
}
@Override public T deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
try {
Constructor constructor = handledType().getConstructor(new Class[] {
Object.class });
Object externalInjected = ctxt.findInjectableValue("constructorAttr", null,
null);
T inst = (T) constructor.newInstance(new Object[] { externalInjected });
// NOW WHAT?
// have to ignore the "@class" property
// super.deserialize(p, ctxt, inst); //Nope. This calls back here
// return remainderDeserialization.deserialize(p, ctxt, inst); //Nope. No
_valueDeserializer assigned
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
}
Beyond the original post introducing ValueInstantiator
<http://www.cowtowncoder.com/blog/archives/2011/10/entry_463.html> , I have
found no example code or tutorials. The unit test code helps understanding,
but didn't have a solution to my problem. It did give me hope that I could
implement the SimpleModule as follows:
public static class TestModule extends SimpleModule { public TestModule() {
super("TestDomain", new Version(1, 0, 0, null, null, null)); } @Override
public void setupModule(SetupContext context) { setValueInstantiators(new
ClassHierarchyLookupValueInstantiators()); super.setupModule(context); } }
static class ClassHierarchyLookupValueInstantiators extends
SimpleValueInstantiators { private static final long serialVersionUID = 1L;
//lazy ValueInstantiator @Override public ValueInstantiator
findValueInstantiator(DeserializationConfig config, BeanDescription
beanDesc, ValueInstantiator defaultInstantiator) { if
(TestDomain.class.isAssignableFrom(beanDesc.getBeanClass())) { ClassKey
classkey = new ClassKey(beanDesc.getBeanClass()); ValueInstantiator inst =
_classMappings.get(classkey); if (inst == null) { inst = new
MyValueInstantiator(beanDesc.getBeanClass()); _classMappings.put(classkey,
inst); } return inst; } return defaultInstantiator; } }
While the above seemed like it would work, when I followed the code to
BeanDeserializerBase.deserializeFromObjectUsingNonDefault(JsonParser p,
DeserializationContext ctxt), it failed because delegateDeser was null,
i.e.,:
protected Object deserializeFromObjectUsingNonDefault(JsonParser p,
DeserializationContext ctxt) throws IOException
{
final JsonDeserializer<Object> delegateDeser =
_delegateDeserializer();
if (delegateDeser != null) {
//NEEDED TO GET HERE, BUT delegateDeser IS NULL
return _valueInstantiator.createUsingDelegate(ctxt,
delegateDeser.deserialize(p, ctxt));
}
//AND THIS IS NULL
if (_propertyBasedCreator != null) {
return _deserializeUsingPropertyBased(p, ctxt);
}
//WE GET HERE AND IT IS TOO LATE
// 25-Jan-2017, tatu: We do not actually support use of Creators
for non-static
// inner classes -- with one and only one exception; that of
default constructor!
// -- so let's indicate it
Class<?> raw = _beanType.getRawClass();
if (ClassUtil.isNonStaticInnerClass(raw)) {
return ctxt.handleMissingInstantiator(raw, null, p,
"can only instantiate non-static inner class by using default, no-argument
constructor");
}
return ctxt.handleMissingInstantiator(raw, getValueInstantiator(),
p,
"cannot deserialize from Object value (no delegate- or
property-based Creator)");
}
Anyone got any better ideas? TIA.
--
You received this message because you are subscribed to the Google Groups
"jackson-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
For more options, visit https://groups.google.com/d/optout.