Lets suppose, that we have a bean like this:
public class Response<T> {
private T data;
private double executionDuration;
private boolean success;
private String version;
//HOW TO Make Jackson to inject this?
private Class<T> dataClass;
public Optional<T> getData() {
return Optional.ofNullable(data);
}
public double getExecutionDuration() {
return executionDuration;
}
public Class<T> getDataClass() {
return dataClass;
}
public String getVersion() {
return version;
}
public boolean isSuccess() {
return success;
}
}
The deserialization happens like this:
objectMapper.readValue(json, new TypeReference<Response<SomeClass>>() {});
Can I somehow make Jackson to inject the class "SomeClass" into my bean? Injecting the type reference itself would be also ok, I think.
If it is undesirable to save class info in json and use #JsonTypeInfo I would suggest to use #JacksonInject:
public class Response<T> {
private T data;
private double executionDuration;
private boolean success;
private String version;
#JacksonInject("dataClass")
private Class<T> dataClass;
public Optional<T> getData() {
return Optional.ofNullable(data);
}
public double getExecutionDuration() {
return executionDuration;
}
public Class<T> getDataClass() {
return dataClass;
}
public String getVersion() {
return version;
}
public boolean isSuccess() {
return success;
}
}
Deserialization would look like:
ObjectMapper mapper = new ObjectMapper();
InjectableValues.Std injectable = new InjectableValues.Std();
injectable.addValue("dataClass", SomeClass.class);
mapper.setInjectableValues(injectable);
final Response<Integer> response = mapper.readValue(json, new TypeReference<Response<SomeClass>>() { });
this worked for me;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
#Getter
#Setter
#NoArgsConstructor
public class Entity<T> {
private T data;
#JsonSerialize(converter = ClassToStringConverter.class)
#JsonDeserialize(converter = StringToClassConverter.class)
private Class<T> dataClass;
}
and
import com.fasterxml.jackson.databind.util.StdConverter;
public class ClassToStringConverter extends StdConverter<Class<?>, String> {
public String convert(Class<?> aClass) {
// class java.lang.Integer
return aClass.toString().split("\\s")[1];
}
}
and
import com.fasterxml.jackson.databind.util.StdConverter;
public class StringToClassConverter extends StdConverter<String, Class<?>> {
public Class<?> convert(String s) {
try {
return Class.forName(s);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
Main;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Entity<Integer> data = new Entity<Integer>();
data.setData(5);
data.setDataClass(Integer.class);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(data);
Entity<Integer> jsonData = mapper.readValue(json, new TypeReference<Entity<Integer>>() {});
System.out.println(jsonData.getData());
System.out.println(jsonData.getDataClass().getCanonicalName());
}
}
But, maybe it will be better, to not save the class type, but use method to get type from data?
public Class<T> getType() {
return (Class<T>) data.getClass();
}
public class Response<T> {
private T data;
// other fields & methods
public Class getType() {
return Optional.ofNullable(data).map(Object::getClass).orElse(Void.class);
}
public Optional<Class> getSafeType() {
return Optional.ofNullable(data).map(Object::getClass);
}
}
Super simple, no need to tinker with Jackson, NPE safe...
Related
Using Jackson, is there a way to deserialize a proprty that depends on the value of another property?
if i have this json {"foo":"a","bar":"b"} i'd like to deserialize it to the Test class below as Test [foo=a, bar=b_a], where bar is the value of the json property "bar" and the value of the property "foo".
Of course this is a trivial example, the real deal would be to deserialize a datamodel entity: {"line":"C12", "machine": {"line":"C12", "code":"A"}} machine.line and line are always the same, and i'd like to express it like this: {"line":"C12", "machine": "A"}
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
public abstract class Main{
private static class Test {
#JsonProperty
private String foo;
#JsonProperty
#JsonDeserialize(using = CustomDeserializer.class)
private String bar;
// ...other fields to be deserialized with default behaviour
private Test() {
}
public Test(String a, String bar) {
this.foo = a;
this.bar = bar;
}
#Override
public String toString() {
return "Test [foo=" + foo + ", bar=" + bar + "]";
}
}
private static class CustomDeserializer extends StdDeserializer<String> {
protected CustomDeserializer() {
super(String.class);
}
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String foo = //how to get foo property?
String value = p.getValueAsString();
if (!foo.isEmpty()) {
return value + "_" + foo;
} else {
return value;
}
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Test foo2 = mapper.readValue("{\"foo\":\"a\",\"bar\":\"b\"}", Test.class);
System.out.println(foo2); // Test [foo=a, bar=b_a]
}
}
One way to solve your problem is specify a custom deserializer that involves your Test class instead of your string field because the deserialization of your property is based on the value of another property:
public class CustomDeserializer extends JsonDeserializer<Test> {}
#JsonDeserialize(using = CustomDeserializer.class)
public class Test {}
Then you can deserialize your object reading the JsonNode tree built from your input string:
public class CustomDeserializer extends JsonDeserializer<Test> {
#Override
public Test deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String foo = node.get("foo").asText();
String bar = node.get("bar").asText();
if (!foo.isEmpty()) {
bar = (bar + '_' + foo);
}
return new Test(foo, bar);
}
}
//your example
public class Main {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
Test foo2 = mapper.readValue("{\"foo\":\"a\",\"bar\":\"b\"}", Test.class);
System.out.println(foo2); // Test [foo=a, bar=b_a]
}
}
I got a similar problem today and I wanted to share my solution. So instead of using a #JsonDeserialize, I use a #JsonCreator on the parent object with a package private constructor to accept the "raw" properties and then I can process this data and return better objects.
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
class Scratch {
public static void main(String[] args) throws Exception{
final var testData = "{\"foo\":\"a\",\"bar\":\"b\"}";
final var mapper = new ObjectMapper();
final var testObj = mapper.readValue(testData, Test.class);
System.out.println(testObj); // Test[foo=a, bar=a_b]
}
record Test (
String foo,
String bar
){
#JsonCreator Test(
#JsonProperty("foo") String foo,
#JsonProperty("bar") String bar,
#JsonProperty("_dummy") String _dummy // extra param for the constructor overloading
) {
this(foo, deserializeBar(foo, bar));
}
private static String deserializeBar(String foo, String bar) {
if (foo == null || foo.isEmpty()) {
return bar;
}
return "%s_%s".formatted(foo, bar);
}
}
}
In the end, I've resorted using BeanDeserializerModifier
Please notice that the following code is not fully functioning because it relies on code I'm not allowed to share, but it should suffice to get the idea.
package com.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
public class JsonDelegateDeserializerModule extends SimpleModule {
// !! must be registered as guice factory
public interface JsonDelegateDeserializerFactory {
JsonDelegateDeserializerModule create(String packagePath);
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonDelegateDeserializer {
public Class<? extends StdDeserializer<?>> deserializer();
public Class<?> forType();
}
protected interface JsonDeserializerFactory {
// non metto nessun generic in TagHandler o guice non riesce piu a creare la
// factory!
#SuppressWarnings("rawtypes")
public JsonDeserializer create(JsonDeserializer baseDeserializer);
}
private static final Logger LOGGER = LoggerFactory.getLogger(JsonDelegateDeserializerModule.class);
#Inject
private FactoryInjector injector;
private final String packagePath;
#AssistedInject
protected JsonDelegateDeserializerModule(#Assisted String packagePath) {
super();
this.packagePath = packagePath;
}
#Override
public String getModuleName() {
return JsonDelegateDeserializerModule.class.getSimpleName() + "[" + packagePath + "]";
}
#Override
public Object getTypeId() {
return JsonDelegateDeserializerModule.class.getSimpleName() + "[" + packagePath + "]";
}
#Override
public void setupModule(SetupContext context) {
Reflections reflectios = new Reflections(packagePath, new SubTypesScanner(), new TypeAnnotationsScanner());
Map<Class<?>, JsonDeserializerFactory> classToDeserializerFactory = new HashMap<>();
Set<Class<?>> classesWithModifier = reflectios.getTypesAnnotatedWith(JsonDelegateDeserializer.class);
for (Class<?> classWithModifier : classesWithModifier) {
JsonDelegateDeserializer annotation = classWithModifier.getAnnotation(JsonDelegateDeserializer.class);
if (annotation != null) {
Class<? extends StdDeserializer<?>> deserializerType = annotation.deserializer();
Class<?> forType = annotation.forType();
try {
JsonDeserializerFactory factory = injector.getFactory(JsonDeserializerFactory.class,
deserializerType);
classToDeserializerFactory.put(forType, factory);
} catch (Exception e) {
LOGGER.error("Exception was thown while creating deserializer {} for type {}:", deserializerType,
forType, e);
throw new RuntimeException(e);
}
}
}
if (!classToDeserializerFactory.isEmpty()) {
setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
List<Class<?>> possibleTypesList = new LinkedList<>();
if (deserializer instanceof BeanDeserializer) {
for (Entry<Class<?>, JsonDeserializerFactory> entry : classToDeserializerFactory.entrySet()) {
Class<?> type = entry.getKey();
if (type.isAssignableFrom(deserializer.handledType())) {
possibleTypesList.add(type);
}
}
if (possibleTypesList.size() > 1) {
possibleTypesList.sort(new Comparator<Class<?>>() {
#Override
public int compare(Class<?> o1, Class<?> o2) {
if (o1.isAssignableFrom(o2)) {
return 1;
} else {
return -1;
}
}
});
}
Class<?> type = Utils.first(possibleTypesList);
if (type == null) {
return super.modifyDeserializer(config, beanDesc, deserializer);
} else {
JsonDeserializerFactory factory = classToDeserializerFactory.get(type);
JsonDeserializer<?> modifiedDeserializer = factory.create(deserializer);
return super.modifyDeserializer(config, beanDesc, modifiedDeserializer);
}
} else {
// è gia stato impostato un deserializzatore piu specifico, non imposato questo
return super.modifyDeserializer(config, beanDesc, deserializer);
}
}
});
}
super.setupModule(context);
}
}
then you can simply annotate the Mixin to add the custom deserializer
#JsonDelegateDeserializer(deserializer = LoadLineDeserializer.class, forType = Line.class)
public interface LineMixIn {
public static class LoadLineDeserializer extends DelegatingDeserializer {
#AssistedInject
public LoadLineDeserializer(#Assisted JsonDeserializer baseDeserializer, LineService lineService) {
super(baseDeserializer);
}
// ...
}
// ...
}
For json mapping I use the following method:
public static <T> T mapJsonToObject(String json, T dtoClass) throws Exception {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, new TypeReference<RestResponse<UserDto>>() {
});
}
And UserDto looks like this:
#JsonIgnoreProperties(ignoreUnknown = true)
public class UserDto {
#JsonProperty("items")
private List<User> userList;
public List<User> getUserList() {
return userList;
}
public void setUserList(List<User> userList) {
this.userList = userList;
}
}
I want to improve this method of mapping without being attached to a UserDto class, and replacing it with a generic.
Is it possible? And How?
Thanks.
TypeReference requires you to specify parameters statically, not dynamically, so it does not work if you need to further parameterize types.
What I think you need is JavaType: you can build instances dynamically by using TypeFactory. You get an instance of TypeFactory via ObjectMapper.getTypeFactory(). You can also construct JavaType instances from simple Class as well as TypeReference.
One approach will be to define a Jackson JavaType representing a list of items of type clazz. You still need to have access to the class of the generic parameter at runtime. The usual approach is something like
<T> class XX { XX(Class<T> clazz, ...) ... }
to pass the class of the generic parameter into the generic class at construction.
Upon access to the Class clazz variable you can construct a Jackson JavaType representing, for example, a list of items of class clazz with the following statement.
JavaType itemType = mapper.getTypeFactory().constructCollectionType(List.class, clazz);
I hope it helped. I am using this approach in my own code.
Try this:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Array;
import java.util.Arrays;
...
public static <TargetType> List<TargetType> convertToList(String jsonString, Class<TargetType> targetTypeClass) {
List<TargetType> listOfTargetObjects = null;
ObjectMapper objectMapper = new ObjectMapper();
TargetType[] arrayOfTargetType = (TargetType[]) Array.newInstance(targetTypeClass, 0);
try {
listOfTargetObjects = (List<TargetType>) Arrays.asList(objectMapper.readValue(jsonString, arrayOfTargetType.getClass()));
} catch (JsonMappingException jsonMappingException) {
listOfTargetObjects = null;
} catch (JsonProcessingException jsonProcessingException) {
listOfTargetObjects = null;
} catch (Exception exception) {
listOfTargetObjects = null;
}
return listOfTargetObjects;
}
...
This is an example of parsing simple List based generics with Jackson, with a simple Java annotation!
package innovate.tamergroup.lastmiledelivery.loader.otm.models;
import java.util.List;
import javax.annotation.Generated;
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({
"hasMore",
"limit",
"count",
"offset",
"items",
"links"
})
#Generated("jsonschema2pojo")
public class OTMListWrapper<T> {
#JsonProperty("hasMore")
private Boolean hasMore;
#JsonProperty("limit")
private Long limit;
#JsonProperty("count")
private Long count;
#JsonProperty("offset")
private Long offset;
#JsonTypeInfo(use=JsonTypeInfo.Id.NONE, include=JsonTypeInfo.As.PROPERTY, property="items")
private List<T> items = null;
#JsonProperty("links")
private List<OTMLink> links = null;
#JsonProperty("hasMore")
public Boolean getHasMore() {
return hasMore;
}
#JsonProperty("hasMore")
public void setHasMore(Boolean hasMore) {
this.hasMore = hasMore;
}
public OTMListWrapper<T> withHasMore(Boolean hasMore) {
this.hasMore = hasMore;
return this;
}
#JsonProperty("limit")
public Long getLimit() {
return limit;
}
#JsonProperty("limit")
public void setLimit(Long limit) {
this.limit = limit;
}
public OTMListWrapper<T> withLimit(Long limit) {
this.limit = limit;
return this;
}
#JsonProperty("count")
public Long getCount() {
return count;
}
#JsonProperty("count")
public void setCount(Long count) {
this.count = count;
}
public OTMListWrapper<T> withCount(Long count) {
this.count = count;
return this;
}
#JsonProperty("offset")
public Long getOffset() {
return offset;
}
#JsonProperty("offset")
public void setOffset(Long offset) {
this.offset = offset;
}
public OTMListWrapper<T> withOffset(Long offset) {
this.offset = offset;
return this;
}
#JsonProperty("items")
public List<T> getItems() {
return items;
}
#JsonProperty("items")
public void setItems(List<T> items) {
this.items = items;
}
public OTMListWrapper<T> withItems(List<T> items) {
this.items = items;
return this;
}
#JsonProperty("links")
public List<OTMLink> getLinks() {
return links;
}
#JsonProperty("links")
public void setLinks(List<OTMLink> links) {
this.links = links;
}
public OTMListWrapper<T> withLinks(List<OTMLink> links) {
this.links = links;
return this;
}
}
I have two classes like this:
public class A {
String aProp = "aProp";
public String getAProp() {
return aProp;
}
}
public class B {
String bProp = "bProp";
A a = new A();
#JsonProperty("bProp")
public String getBProp() {
return bProp;
}
#JsonSerialize(using = CustomSerializer.class)
public A getA() {
return a;
}
}
I'm expecting to get JSON like this:
{
"bProp": "bProp", // just serizlised bProp
"sProp1": "sProp1_aProp", // computed using aProp
"sProp2": "sProp2_aProp" // computed another way
}
So I wrote custom JsonSerializer like this:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class CustomSerializer extends JsonSerializer<A> {
#Override
public void serialize(A a, JsonGenerator json, SerializerProvider provider) throws IOException {
json.writeStringField("sProp1", "sProp1_" + a.getAProp());
json.writeStringField("sProp2", "sProp2_" + a.getAProp());
}
}
But I keep getting an error:
com.fasterxml.jackson.core.JsonGenerationException: Can not write a field name, expecting a value
Unless I put json.writeStartObject(); and json.writeEndObject(); in serialize method (so it produces wrong JSON).
So I'm looking for a solution like #JsonUnwrapped to use with custom JsonSerializer.
I understand your problem and the thing that you need is UnwrappingBeanSerializer. You can see another related SO post:
Different JSON output when using custom json serializer in Spring Data Rest
The problem is that you cannot have both annotations #JacksonUnwrapped and #JsonSerialize in one field because when you have #JsonSerializer Jackson will always write field name.
Here is the complete solution:
public class CustomSerializer extends UnwrappingBeanSerializer {
public CustomSerializer(BeanSerializerBase src, NameTransformer transformer) {
super(src, transformer);
}
#Override
public JsonSerializer<Object> unwrappingSerializer(NameTransformer transformer) {
return new CustomSerializer(this, transformer);
}
#Override
protected void serializeFields(Object bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
A a = (A) bean;
jgen.writeStringField("custom", a.getAProp());
jgen.writeStringField("custom3", a.getAProp());
}
#Override
public boolean isUnwrappingSerializer() {
return true;
}
}
Test case, you should redefine your object mapper with custom configuration or research for other method .
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#SpringApplicationConfiguration(classes = Application.class)
public class ColorsTest {
ObjectMapper mapper = new ObjectMapper();
#Before
public void setUp(){
mapper.registerModule(new Module() {
#Override
public String getModuleName() {
return "my.module";
}
#Override
public Version version() {
return Version.unknownVersion();
}
#Override
public void setupModule(SetupContext context) {
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if(beanDesc.getBeanClass().equals(A.class)) {
return new CustomSerializer((BeanSerializerBase) serializer, NameTransformer.NOP);
}
return serializer;
}
});
}
});
}
#Test
public void testSerializer() throws JsonProcessingException {
System.out.println(mapper.writeValueAsString(new B()));
}
}
Class B:
public class B {
#JsonProperty("bProp")
public String getBProp() {
return "bProp";
}
#JsonUnwrapped
public A getA() {
return new A();
}
}
I like to add this post and solution to the question asked here: Using custom Serializers with JsonUnwrapperd as the original poster is using JsonSerializer as I am. The suggest approach with the UnwrappingBeanSerializer won't work in this case. My post has a slightly different goal, but the idea from the post should be applicable to your use case easily, as it is just overwriting one more method and not having to add bunch of stuff apart from JsonUnwrapped on the property.
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
public class Test {
static class A {
String aProp = "aProp";
public String getAProp() {
return aProp;
}
}
static class B {
String bProp = "bProp";
A a = new A();
#JsonProperty("bProp")
public String getBProp() {
return bProp;
}
#JsonSerialize(using = CustomSerializer.class)
#JsonUnwrapped
public A getA() {
return a;
}
}
static class CustomSerializer extends JsonSerializer<A> {
#Override
public boolean isUnwrappingSerializer() {
return true;
}
#Override
public void serialize(A a, JsonGenerator json, SerializerProvider provider) throws IOException {
json.writeStringField("sProp1", "sProp1_" + a.getAProp());
json.writeStringField("sProp2", "sProp2_" + a.getAProp());
}
}
public static void main(String... a) throws Exception {
final ObjectMapper o = new ObjectMapper();
o.enable(SerializationFeature.INDENT_OUTPUT);
System.out.println(o.writeValueAsString(new B()));
}
}
I am trying to use GraphAdapterBuilder which is an extra to the GSON library to serialize an object with cyclic references. It works great for class but fails when trying to deserialize an interface.
To deserialize interface( which GSON doesn't do by default ) I am using PropertyBasedInterfaceMarshal or InterfaceAdapter. These are registered as custom type adapters for the interfaces.
When using ether above both fail to deserialize the interface as they are only passed the graph id like "0x4" as generated by GraphAdapterBuilder. This is passed as the JsonElement in the deserializer. Obviously there is nothing that can be done with this id from within the deserializer.
Shouldn't these be caught by the GraphAdapterBuilder instead of trying to be deserialized? I have not been able to get around this, is this a bug with GraphAdapterBuilder or is there a way to get around this?
Ok, this is a (working) stub for a solution. It's too late in Italy, to make it nicer.
You need a delegate function like this
package com.google.gson.graph;
/**
* #author Giacomo Tesio
*/
public interface GenericFunction<Domain, Codomain> {
Codomain map(Domain domain);
}
a TypeAdapterFactory like this:
package com.google.gson.graph;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* #author Giacomo Tesio
*/
public class InterfaceAdapterFactory implements TypeAdapterFactory {
final Map<String, GenericFunction<Gson, TypeAdapter<?>>> adapters;
private final Class<?> commonInterface;
public InterfaceAdapterFactory(Class<?> commonInterface, Class<?>[] concreteClasses)
{
this.commonInterface = commonInterface;
this.adapters = new HashMap<String, GenericFunction<Gson, TypeAdapter<?>>>();
final TypeAdapterFactory me = this;
for(int i = 0; i < concreteClasses.length; ++i)
{
final Class<?> clazz = concreteClasses[i];
this.adapters.put(clazz.getName(), new GenericFunction<Gson, TypeAdapter<?>>(){
public TypeAdapter<?> map(Gson gson) {
TypeToken<?> type = TypeToken.get(clazz);
return gson.getDelegateAdapter(me, type);
}
});
}
}
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
if(!this.commonInterface.isAssignableFrom(type.getRawType())
&& !this.commonInterface.equals(type.getRawType()))
{
return delegate;
}
final TypeToken<T> typeToken = type;
final Gson globalGson = gson;
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
out.beginObject();
out.name("#t");
out.value(value.getClass().getName());
out.name("#v");
delegate.write(out, value);
out.endObject();
}
#SuppressWarnings({"unchecked"})
public T read(JsonReader in) throws IOException {
JsonToken peekToken = in.peek();
if(peekToken == JsonToken.NULL) {
in.nextNull();
return null;
}
in.beginObject();
String dummy = in.nextName();
String typeName = in.nextString();
dummy = in.nextName();
TypeAdapter<?> specificDelegate = adapters.get(typeName).map(globalGson);
T result = (T)specificDelegate.read(in);
in.endObject();
return result;
}
};
}
}
a pair of tests like these
public final class InterfaceAdapterFactoryTest extends TestCase {
public void testInterfaceSerialization1(){
SampleInterface first = new SampleImplementation1(10);
SampleInterfaceContainer toSerialize = new SampleInterfaceContainer("container", first);
GsonBuilder gsonBuilder = new GsonBuilder();
new GraphAdapterBuilder()
.addType(SampleInterfaceContainer.class)
.addType(SampleImplementation1.class)
.addType(SampleImplementation2.class)
.registerOn(gsonBuilder);
gsonBuilder.registerTypeAdapterFactory(new InterfaceAdapterFactory(
SampleInterface.class, new Class<?>[] { SampleImplementation1.class, SampleImplementation2.class }
));
Gson gson = gsonBuilder.create();
String json = gson.toJson(toSerialize);
System.out.println(json);
SampleInterfaceContainer deserialized = gson.fromJson(json, SampleInterfaceContainer.class);
assertNotNull(deserialized);
assertEquals(toSerialize.getName(), deserialized.getName());
assertEquals(toSerialize.getContent().getNumber(), deserialized.getContent().getNumber());
}
public void testInterfaceSerialization2(){
SampleImplementation2 first = new SampleImplementation2(5, "test");
SampleInterfaceContainer toSerialize = new SampleInterfaceContainer("container", first);
first.Container = toSerialize;
GsonBuilder gsonBuilder = new GsonBuilder();
new GraphAdapterBuilder()
.addType(SampleInterfaceContainer.class)
.addType(SampleImplementation1.class)
.addType(SampleImplementation2.class)
.registerOn(gsonBuilder);
gsonBuilder.registerTypeAdapterFactory(new InterfaceAdapterFactory(
SampleInterface.class, new Class<?>[] { SampleImplementation1.class, SampleImplementation2.class }
));
Gson gson = gsonBuilder.create();
String json = gson.toJson(toSerialize);
System.out.println(json);
SampleInterfaceContainer deserialized = gson.fromJson(json, SampleInterfaceContainer.class);
assertNotNull(deserialized);
assertEquals(toSerialize.getName(), deserialized.getName());
assertEquals(5, deserialized.getContent().getNumber());
assertEquals("test", ((SampleImplementation2)deserialized.getContent()).getName());
assertSame(deserialized, ((SampleImplementation2)deserialized.getContent()).Container);
}
}
and some sample classes (to verify that the tests pass)
public class SampleInterfaceContainer {
private SampleInterface content;
private String name;
public SampleInterfaceContainer(String name, SampleInterface content)
{
this.name = name;
this.content = content;
}
public String getName()
{
return this.name;
}
public SampleInterface getContent()
{
return this.content;
}
}
public interface SampleInterface {
int getNumber();
}
public class SampleImplementation1 implements SampleInterface{
private int number;
public SampleImplementation1()
{
this.number = 0;
}
public SampleImplementation1(int number)
{
this.number = number;
}
public int getNumber()
{
return this.number;
}
}
public class SampleImplementation2 implements SampleInterface{
private int number;
private String name;
public SampleInterfaceContainer Container;
public SampleImplementation2()
{
this.number = 0;
this.name = "";
}
public SampleImplementation2(int number, String name)
{
this.number = number;
this.name = name;
}
public int getNumber()
{
return this.number;
}
public String getName()
{
return this.name;
}
}
While this has been a quick&dirty hack, it works like a charme.
You just have to pay attention at the order of the operations during the initialization of GsonBuilder. You have to initialize and register the GraphAdapterBuilder first and only after register this factory.
It has been funny (if a bit tricky since I'm not a Java expert).
i have a question concerning Json deserialization with Jackson (edit: 2.0.4 version). I would like serialize an Bean, containing the list of other beans, as string , save this string and then to deserialize later this string. I use the some base class and its subtypes. The basis class Parent is an abstract class, that has two attributes with getters und setters, this class has also an abstract method getType(). Other abstract class AbstractChild inherits from class Parent . This class has attributes too and isExportEnabled() abstract method.
I have no problems, if this Bean will be serialized. I use the following annotation on the Parent class
*#JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#cls")*
The string will be generated.
But the desirialize failed: the exception “Unrecognized field "type"”will be thrown. But I need this attribute! I’m tried to set #JsonProperty("type") on abstract method, this has no effect.
Please help me.
edit: if i introduce the private fields "type" (Parent) and "exportEnabled" (AbstractChild) so it runs correctly.
P.S The exception
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException:
Unrecognized field "type" (class tst.SimpleTestMain$FirstChild), not
marked as ignorable (4 known properties: , "id", "maxCount", "code",
"minCount"]) at [Source: java.io.StringReader#1ad9fa; line: 1,
column: 125] (through reference chain:
tst.Fam["members"]->tst.FirstChild["type"]) at
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:79)
at
com.fasterxml.jackson.databind.DeserializationContext.reportUnknownProperty(DeserializationContext.java:568)
…and the example class
package tst;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SimpleTestMain {
enum Type {
TYPE_A, TYPE_B
}
#JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#cls")
public static abstract class Parent {
private int id;
private String code;
public Parent() {
}
#JsonProperty("type")
// First abstract getter
public abstract Type getType();
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
public static abstract class AbstractChild extends Parent {
private int minCount;
private int maxCount;
public AbstractChild() {
}
// Second abstract method: boolean used
public abstract boolean isExportEnabled();
public int getMinCount() {
return minCount;
}
public void setMinCount(int minCount) {
this.minCount = minCount;
}
public int getMaxCount() {
return maxCount;
}
public void setMaxCount(int maxCount) {
this.maxCount = maxCount;
}
}
public static class FirstChild extends AbstractChild {
#Override
public boolean isExportEnabled() {
return false;
}
#Override
public Type getType() {
return Type.TYPE_A;
}
}
public static class SecondChild extends AbstractChild {
#Override
public boolean isExportEnabled() {
return true;
}
#Override
public Type getType() {
return Type.TYPE_B;
}
}
public static class Fam {
private int famId;
private List<Parent> members;
public Fam() {
members = new ArrayList<Parent>();
}
public int getFamId() {
return famId;
}
public void setFamId(int famId) {
this.famId = famId;
}
public List<Parent> getMembers() {
return members;
}
public void setMembers(List<Parent> members) {
this.members = members;
}
public void addMember(Parent member) {
members.add(member);
}
}
public SimpleTestMain() {
}
public static void main(String[] args) {
Fam fam = new Fam();
FirstChild fc = new FirstChild();
fc.setId(1);
fc.setCode("FirstChildCode");
fc.setMinCount(1);
fc.setMaxCount(4);
fam.addMember(fc);
SecondChild sc = new SecondChild();
sc.setCode("SecondChildCode");
sc.setMinCount(131);
sc.setMaxCount(431);
fam.addMember(sc);
String test = "";
// Serialize it
ObjectMapper mapper = new ObjectMapper();
try {
test = mapper.writeValueAsString(fam);
System.out.println("Serialized bean:\n" + test);
// the output
// Serialized bean:
// {"famId":0,"members":[{"#cls":".SimpleTestMain$FirstChild","id":1,"code":"FirstChildCode","minCount":1,"maxCount":4,"type":"TYPE_A","exportEnabled":false},{"#cls":".SimpleTestMain$SecondChild","id":0,"code":"SecondChildCode","minCount":131,"maxCount":431,"type":"TYPE_B","exportEnabled":true}]}
} catch (IOException e) {
e.printStackTrace();
}
// Deserialize it
mapper = new ObjectMapper();
// mapper.enableDefaultTyping();
try {
Fam fam1 = mapper.readValue(test, Fam.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}