Jackson deserialize objects as the same one - java

I was working with jackson's JsonIdentityInfo annatation, the serialization looks good. But the deserialization didnt work as I expected, the objects sharing the same id, are not deserialized into the same objects. Below are the class definitions.
class ParameterResolver implements ObjectIdResolver {
private final Map<ObjectIdGenerator.IdKey,Object> items = new HashMap<>();
#Override
public void bindItem(ObjectIdGenerator.IdKey id, Object pojo) {
if (items.containsKey(id)) {
throw new IllegalStateException("Already had POJO for id (" + id.key.getClass().getName() + ") [" + id
+ "]");
}
items.put(id, pojo);
}
#Override
public Object resolveId(ObjectIdGenerator.IdKey id) {
Object object = items.get(id);
return object == null ? getById(id) : object;
}
protected Object getById(ObjectIdGenerator.IdKey id){
Object object;
try {
object = id.scope.getConstructor().newInstance();
id.scope.getMethod("setId", Integer.class).invoke(object, (Integer) id.key);
} catch (Exception e) {
throw new IllegalStateException(e);
}
items.put(id, object);
return object;
}
#Override
public ObjectIdResolver newForDeserialization(Object context) {
return new ParameterResolver();
}
#Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return resolverType.getClass() == getClass();
}
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, resolver = ParameterResolver.class, property = "id", scope = Parameter.class)
class Parameter {
private Integer id;
private String data;
public Parameter() {}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
class Container {
public Parameter p;
public Container() {}
public Container(Parameter p) {
this.p = p;
}
}
and this is the unit test
#Test
public void test() throws JsonProcessingException {
Parameter p1 = new Parameter(), p2 = new Parameter();
p1.setId(1);
p1.setData("1");
List<Container> list = new ArrayList<>();
list.add(new Container(p1));
list.add(new Container(p1));
ObjectMapper mapper = new ObjectMapper();
String content = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(list);
List<Container> listD = mapper.readValue(content, new TypeReference<List<Container>>() {});
assertSame(listD.get(0).p, list.get(1).p); // didnt pass this assertion
}

Deserialization creates new objects from JSON. Objects sharing the same id are deserialized into the same objects. However, these are different objects than those that were previously serialized.
In your assertion you are comparing a deserialized parameter instance with an instance that was used for serialization:
assertSame(listD.get(0).p, list.get(1).p); // didnt pass this assertion
You should compare the two deserialized instances instead:
assertSame(listD.get(0).p, listD.get(1).p);
Please note the 'D' in the second parameter listD.get(1).p.

Related

Jackson Databind ObjectMapper convertValue with custom mapping implementaion

Foo1
public class Foo1{
private Long id;
private String code;
private String name;
private Boolean rState;
private String comments; // Contain json data
private String attachments; // Contain json data
}
Foo2
public class Foo2{
private Long id;
private String code;
private String name;
private Boolean rState;
private List comments;
private List attachments;
}
convert value
new ObjectMapper().convertValue(foo1, Foo2.class);
is it possible, when I call convert value, json String automatically convert to list ?
I would recommend going with a Deserializer as Foo1 might actually be serialized elsewhere and we might only have it's serialized data to convert. And that is what is our goal here.
Create a SimpleModule that deserializes List type
public class ListDeserializer extends StdDeserializer<List> {
public ListDeserializer() {
super(List.class);
}
#Override
public List deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
if (p.getCurrentName().equals("result") || p.getCurrentName().equals("attachments")) {
JsonNode node = p.getCodec().readTree(p);
return new ObjectMapper().readValue(node.asText(), List.class);
}
return null;
}
}
The above deserializer only recognizes result and attachments and ignores the rest. So this is a very specific deserializer for List in Foo2 class.
My test Foo1 and Foo2 are as follows
public class Foo1 {
String result = "[\"abcd\", \"xyz\"]";
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
public class Foo2 {
List result;
public List getResult() {
return result;
}
public void setResult(List result) {
this.result = result;
}
}
I tested the above code as follows
// Module to help deserialize List objects
SimpleModule listModule = new SimpleModule();
listModule.addDeserializer(List.class, new ListDeserializer());
ObjectMapper objectMapper = new ObjectMapper().registerModules(listModule);
Foo2 foo2 = objectMapper.convertValue(new Foo1(), Foo2.class);
System.out.println(foo2.getResult());
And the output is
[abcd, xyz]
Option 1 : Just annotate the getter method of the Foo1.class
#JsonProperty("comments")
public List getComments() {
List list = new ArrayList();
list.add(comments);
return list;
}
#JsonProperty("attachments")
public List getAttachments() {
List list = new ArrayList();
list.add(attachments);
return list;
}
Foo1 foo1 = new Foo1(Long.valueOf(1),"a","aaa",true,"abc","def");
System.out.println(new ObjectMapper().writeValueAsString(foo1));
Foo2 foo2 = new ObjectMapper().convertValue(foo1, Foo2.class);
System.out.println(new ObjectMapper().writeValueAsString(foo2));
Option 2 : use jackson-custom-serialization
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Foo1.class,new ListSerializer());
mapper.registerModule(simpleModule);
public class ListSerializer extends StdSerializer<Foo1> {
public ListSerializer() {
this(null);
}
protected ListSerializer(Class<Blah.Foo1> t) {
super(t);
}
public void serialize(Blah.Foo1 foo1, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
List list = new ArrayList();
list.add(foo1.getComments());
List list1 = new ArrayList();
list1.add(foo1.getAttachments());
jsonGenerator.writeObjectField("id",foo1.getId());
jsonGenerator.writeObjectField("code",foo1.getCode());
jsonGenerator.writeObjectField("name",foo1.getName());
jsonGenerator.writeObjectField("rState",foo1.getrState());
jsonGenerator.writeObjectField("comments",list);
jsonGenerator.writeObjectField("attachments",list1);
jsonGenerator.writeEndObject();
}
}
Foo1 foo1 = new Foo1(Long.valueOf(1),"a","aaa",true,"abc","def");
System.out.println(mapper.writeValueAsString(foo1));
Foo2 foo2 = mapper.convertValue(foo1, Foo2.class);
System.out.println(new ObjectMapper().writeValueAsString(foo2));

Java GSON - serialize int as strings to json file

I have this Java class:
class Car {
int mileage;
int id;
}
When I tell gson to serialize it, it of course serializes it to:
{
"mileage": 123,
"id": 12345678
}
But what if I want to serialize it to:
{
"mileage": "123",
"id": "12345678"
}
Assuming changing my members from int to String, is not an option, is there a way to tell gson to serialize those int members as strings to the json file?
There are likely many ways to achieve what you desire.
I will share two ways.
FIRST - Using Custom Serialization
SECOND - Using JsonAdapter Annotation - More Simple
Using a custom serialization
public static class CarSerializer implements JsonSerializer<Car> {
public JsonElement serialize(final Car car, final Type type, final JsonSerializationContext context) {
JsonObject result = new JsonObject();
result.add("mileage", new JsonPrimitive(Integer.toString(car.getMileage())));
result.add("id", new JsonPrimitive(Integer.toString(car.getId())));
return result;
}
}
To call this, simply adapt your code or use the following code with a constructor
Car c = new Car(123, 123456789);
com.google.gson.Gson gson = new
GsonBuilder().registerTypeAdapter(Car.class, new CarSerializer())
.create();
System.out.println(gson.toJson(c));
The output should be
{"mileage":"123","id":"12345678"}
Full Code for Example 1:
public class SerializationTest {
public static class Car {
public int mileage;
public int id;
public Car(final int mileage, final int id) {
this.mileage = mileage;
this.id = id;
}
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
public String getMileage() {
return mileage;
}
public void setMileage(final String mileage) {
this.mileage = mileage;
}
}
public static class CarSerializer implements JsonSerializer<Car> {
public JsonElement serialize(final Car car, final Type type, final JsonSerializationContext context) {
JsonObject result = new JsonObject();
result.add("mileage", new JsonPrimitive(Integer.toString(car.getMileage())));
result.add("id", new JsonPrimitive(Integer.toString(car.getId())));
return result;
}
}
public static void main(final String[] args) {
Car c = new Car(123, 123456789);
com.google.gson.Gson gson = new
GsonBuilder().registerTypeAdapter(Car.class, new CarSerializer())
.create();
System.out.println(gson.toJson(c));
}
}
Using a #JsonAdapter annotation
Use the JsonAdapter Annotation on the Car class
#JsonAdapter(CarAdapter.class)
public class Car {
public int mileage;
public int id;
}
Create the Custom Adapter
public class CarAdapter extends TypeAdapter<Car> {
#Override
public void write(JsonWriter writer, Car car) throws IOException {
writer.beginObject();
writer.name("mileage").value(car.getMileage());
writer.name("id").value(car.getId());
writer.endObject();
}
#Override
public Car read(JsonReader in) throws IOException {
// do something you need
return null;
}
}
To serialize, using this method, use something like this
Car c = new Car(123, 123456789);
Gson gson = new Gson();
String result = gson.toJson(c);
Printing result in this case should output
{"mileage":"123","id":"12345678"}
You may try it this way:
new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
.registerTypeAdapter(Integer.class, (JsonSerializer<Integer>)
(integer, type, jsonSerializationContext) -> new
JsonPrimitive(String.valueOf(integer)))
.excludeFieldsWithoutExposeAnnotation().create();

Jackson : map nested object

Using jackson, i wonder if it's possible du map json to Java with nested Object that are not like the json structure.
Here an exemple of what i want to do.
Json :
{
a = "someValue",
b = "someValue",
c = "someValue"
}
Java :
public class AnObject {
#JsonProperty("a")
private String value;
//Nested object
private SomeObject;
}
public class SomeObject {
#JsonProperty("b")
private String value1;
#JsonProperry("c")
private String value2;
}
Is it possible ?
Use the JsonUnwrapped annotation:
#JsonUnwrapped
private final SomeObject someObject;
which unwrappes all of SomeObject's fields into the parent, resulting in the following when serializing:
{"a":"foo","b":"bar","c":"baz"}
Using ObjectMapper you can convert JSON string to Object.
Use JsonUnwrapped in your AnObject class over someObject field.
#JsonUnwrapped
private SomeObject someObject;
then read JSON string and convert it to AnObject.
ObjectMapper mapper = new ObjectMapper();
try {
AnObject anObject1 = mapper.readValue(jsonString, AnObject.class);
} catch (IOException e) {
e.printStackTrace();
}
First of all, this is a JSON object.
It's an object literal.
Second of all, that is not a valid formatted object literal.
The correct one is this:
{ "a" : "someValue", "b": "someValue", "c": "someValue"}
Next, as sayd in comments, you have to define your own deserializer.
Main:
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
String json = "{\"a\" : \"someValue\",\"b\" : \"someValue\",\"c\" : \"someValue\"}";
final ObjectMapper om =
new ObjectMapper();//
om.registerSubtypes(AnObject.class);
SimpleModule module = new SimpleModule();
module.addDeserializer(AnObject.class, new CustomDeserializer2());
om.registerModule(module);
AnObject ob = om.readValue(json, AnObject.class);
System.out.println(ob.getValue());
System.out.println(ob.getObject().getValue1());
System.out.println(ob.getObject().getValue2());
}
Deserializer:
public class CustomDeserializer2 extends StdDeserializer<AnObject> {
private static final long serialVersionUID = -3483096770025118080L;
public CustomDeserializer2() {
this(null);
}
public CustomDeserializer2(Class<?> vc) {
super(vc);
}
#Override
public AnObject deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode interNode = jp.getCodec().readTree(jp);
AnObject ob = new AnObject();
if (interNode.get("a") != null) {
ob.setValue(interNode.get("a").toString());
}
SomeObject obj = new SomeObject();
if (interNode.get("b") != null) {
obj.setValue1(interNode.get("b").toString());
}
if (interNode.get("c") != null) {
obj.setValue2(interNode.get("c").toString());
}
ob.setObject(obj);
return ob;
}
Model: Pay attention to #JsonProperty on A field
public class AnObject {
#JsonProperty("a")
private String value;
private SomeObject object;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public SomeObject getObject() {
return object;
}
public void setObject(SomeObject arg) {
object = arg;
}
public class SomeObject {
private String value1;
private String value2;
public String getValue1() {
return value1;
}
public void setValue1(String value1) {
this.value1 = value1;
}
public String getValue2() {
return value2;
}
public void setValue2(String value2) {
this.value2 = value2;
}
Bye

Deserializing transient fields with XStream 1.4.2

I've faced with a requirement to deserialize fields that possibly can be transient using XStream 1.4.2. Despite of that, such fields may be annotated with both #XStreamAlias and #XStreamAsAttribute. Yes, I know, it sounds weird, and this is an indicator of bad design, but this is what I currently have. Since XStream offers a way to specify custom converter, I tried to extend com.thoughtworks.xstream.converters.reflection.ReflectionConverter in order to override the default way of omitting all transient fields trying to make XStream allow to deserialize them. However, I've fully stuck having two ideas to implement such a converter, but none of them works. So here is what I tried:
The 1st way doesn't work:
public final class TransientSimpleConverter extends ReflectionConverter {
private final Class<?> type;
private TransientSimpleConverter(Class<?> type, Mapper mapper, ReflectionProvider reflectionProvider) {
super(mapper, reflectionProvider);
this.type = type;
}
public static TransientSimpleConverter transientSimpleConverter(Class<?> type, XStream xStream) {
return new TransientSimpleConverter(type, xStream.getMapper(), xStream.getReflectionProvider());
}
#Override
protected boolean shouldUnmarshalTransientFields() {
return true;
}
#Override
public boolean canConvert(Class type) {
return this.type == type;
}
}
The 2nd way doesn't work either:
public final class TransientComplexConverter extends ReflectionConverter {
private final Class<?> type;
private TransientComplexConverter(Class<?> type, Mapper mapper, ReflectionProvider provider) {
super(mapper, provider);
this.type = type;
}
public static TransientComplexConverter transientComplexConverter(Class<?> type, Mapper mapper, Iterable<String> fieldNames) {
return new TransientComplexConverter(type, mapper, TransientHackReflectionProvider.transientHackReflectionProvider(type, fieldNames));
}
#Override
public boolean canConvert(Class type) {
return this.type == type;
}
private static final class TransientHackReflectionProvider extends PureJavaReflectionProvider {
private final Class<?> type;
private final Collection<Field> allowedFields;
private final Collection<String> allowedAliases;
private TransientHackReflectionProvider(Class<?> type, Collection<Field> allowedFields, Collection<String> allowedAliases) {
this.type = type;
this.allowedFields = allowedFields;
this.allowedAliases = allowedAliases;
}
public static TransientHackReflectionProvider transientHackReflectionProvider(final Class<?> type, Iterable<String> fieldNames) {
final Collection<Field> allowedFields = from(fieldNames).transform(new Function<String, Field>() {
#Override
public Field apply(String name) {
return field(type, name);
}
}).toList();
final Collection<String> allowedAliases = transform(allowedFields, new Function<Field, String>() {
#Override
public String apply(Field f) {
return f.getName();
}
});
return new TransientHackReflectionProvider(type, allowedFields, allowedAliases);
}
#Override
protected boolean fieldModifiersSupported(Field field) {
return allowedFields.contains(field) ? true : super.fieldModifiersSupported(field);
}
#Override
public boolean fieldDefinedInClass(String fieldName, Class type) {
return type == this.type && allowedAliases.contains(fieldName) ? true : super.fieldDefinedInClass(fieldName, type);
}
private static final Field field(Class<?> type, String name) {
try {
final Field field = type.getDeclaredField(name);
checkArgument(isTransient(field.getModifiers()), name + " is not transient");
checkArgument(field.getAnnotation(XStreamAsAttribute.class) != null, name + " must be annotated with XStreamAsAttribute");
checkArgument(field.getAnnotation(XStreamAlias.class) != null, name + " must be annotated with XStreamAlias");
return field;
} catch (final SecurityException ex) {
throw new RuntimeException(ex);
} catch (final NoSuchFieldException ex) {
throw new RuntimeException(ex);
}
}
}
}
Any suggestions or ideas for a workaround? Thanks in advance.
I know this post is old, but maybe someone is still interested. My solution:
XStream xstream = new XStream(new MyPureJavaReflectionProvider());
class MyPureJavaReflectionProvider extends PureJavaReflectionProvider {
public MyPureJavaReflectionProvider() {
this(new FieldDictionary(new ImmutableFieldKeySorter()));
}
public MyPureJavaReflectionProvider(FieldDictionary fieldDictionary) {
super(fieldDictionary);
}
protected boolean fieldModifiersSupported(Field field) {
int modifiers = field.getModifiers();
return !Modifier.isStatic(modifiers);
}
public boolean fieldDefinedInClass(String fieldName, Class type) {
Field field = fieldDictionary.fieldOrNull(type, fieldName, null);
return field != null && fieldModifiersSupported(field);
}
}

Gson: can we get the serialized field name in a type adapter?

I've seen that the default TypeAdapter for Enum doesn't fit my need:
private static final class EnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> {
private final Map<String, T> nameToConstant = new HashMap<String, T>();
private final Map<T, String> constantToName = new HashMap<T, String>();
public EnumTypeAdapter(Class<T> classOfT) {
try {
for (T constant : classOfT.getEnumConstants()) {
String name = constant.name();
SerializedName annotation = classOfT.getField(name).getAnnotation(SerializedName.class);
if (annotation != null) {
name = annotation.value();
}
nameToConstant.put(name, constant);
constantToName.put(constant, name);
}
} catch (NoSuchFieldException e) {
throw new AssertionError();
}
}
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
return nameToConstant.get(in.nextString());
}
public void write(JsonWriter out, T value) throws IOException {
out.value(value == null ? null : constantToName.get(value));
}
}
If the Enum has value ONE and TWO, when we try to parse THREE, then this value is unknown and Gson will map null instead of raising a parsing exception. I need something more fail-fast.
But I also need something which permits me to know the name of the field which is currently read and creates a parsing failure.
Is it possible with Gson?
Yes.
Gson is quite modular to allow you to use your own TypeAdapterFactory for the enum case. Your custom adapter will return your own EnumTypeAdapter and manage the wanted case. Let the code speak.
package stackoverflow.questions.q16715117;
import java.io.IOException;
import java.util.*;
import com.google.gson.*;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.*;
public class Q16715117 {
public static void main(String[] args) {
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapterFactory(CUSTOM_ENUM_FACTORY);
Container c1 = new Container();
Gson g = gb.create();
String s1 = "{\"colour\":\"RED\",\"number\":42}";
c1 = g.fromJson(s1, Container.class);
System.out.println("Result: "+ c1.toString());
}
public static final TypeAdapterFactory CUSTOM_ENUM_FACTORY = newEnumTypeHierarchyFactory();
public static TypeAdapterFactory newEnumTypeHierarchyFactory() {
return new TypeAdapterFactory() {
#SuppressWarnings({"rawtypes", "unchecked"})
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Class<? super T> rawType = typeToken.getRawType();
if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) {
return null;
}
if (!rawType.isEnum()) {
rawType = rawType.getSuperclass(); // handle anonymous subclasses
}
return (TypeAdapter<T>) new CustomEnumTypeAdapter(rawType);
}
};
}
private static final class CustomEnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> {
private final Map<String, T> nameToConstant = new HashMap<String, T>();
private final Map<T, String> constantToName = new HashMap<T, String>();
private Class<T> classOfT;
public CustomEnumTypeAdapter(Class<T> classOfT) {
this.classOfT = classOfT;
try {
for (T constant : classOfT.getEnumConstants()) {
String name = constant.name();
SerializedName annotation = classOfT.getField(name).getAnnotation(SerializedName.class);
if (annotation != null) {
name = annotation.value();
}
nameToConstant.put(name, constant);
constantToName.put(constant, name);
}
} catch (NoSuchFieldException e) {
throw new AssertionError();
}
}
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
String nextString = in.nextString();
T enumValue = nameToConstant.get(nextString);
if (enumValue == null)
throw new GsonEnumParsinException(nextString, classOfT.getName());
return enumValue;
}
public void write(JsonWriter out, T value) throws IOException {
out.value(value == null ? null : constantToName.get(value));
}
}
}
Plus I declared a custom runtime exception:
public class GsonEnumParsinException extends RuntimeException {
String notFoundEnumValue;
String enumName;
String fieldName;
public GsonEnumParsinException(String notFoundEnumValue, String enumName) {
this.notFoundEnumValue = notFoundEnumValue;
this.enumName = enumName;
}
#Override
public String toString() {
return "GsonEnumParsinException [notFoundEnumValue="
+ notFoundEnumValue + ", enumName=" + enumName + "]";
}
public String getNotFoundEnumValue() {
return notFoundEnumValue;
}
#Override
public String getMessage() {
return "Cannot found " + notFoundEnumValue + " for enum " + enumName;
}
}
These are the classes I used in the example:
public enum Colour {
WHITE, YELLOW, BLACK;
}
public class Container {
private Colour colour;
private int number;
public Colour getColour() {
return colour;
}
public void setColour(Colour colour) {
this.colour = colour;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
#Override
public String toString() {
return "Container [colour=" + colour + ", number=" + number + "]";
}
}
This gives this stacktrace:
Exception in thread "main" GsonEnumParsinException [notFoundEnumValue=RED, enumName=stackoverflow.questions.q16715117.Colour]
at stackoverflow.questions.q16715117.Q16715117$CustomEnumTypeAdapter.read(Q16715117.java:77)
at stackoverflow.questions.q16715117.Q16715117$CustomEnumTypeAdapter.read(Q16715117.java:1)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:93)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:172)
at com.google.gson.Gson.fromJson(Gson.java:803)
at com.google.gson.Gson.fromJson(Gson.java:768)
at com.google.gson.Gson.fromJson(Gson.java:717)
at com.google.gson.Gson.fromJson(Gson.java:689)
at stackoverflow.questions.q16715117.Q16715117.main(Q16715117.java:22)
Unfortunately, the EnumTypeAdapter does not know anything about the context it's called, so this solution is not enough to catch the field name.
Edit
So you have to use also another TypeAdapter that I called CustomReflectiveTypeAdapterFactory and is almost a copy of CustomReflectiveTypeAdapterFactory and I changed a bit the exception, so:
public final class CustomReflectiveTypeAdapterFactory implements TypeAdapterFactory {
private final ConstructorConstructor constructorConstructor;
private final FieldNamingStrategy fieldNamingPolicy;
private final Excluder excluder;
public CustomReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor,
FieldNamingStrategy fieldNamingPolicy, Excluder excluder) {
this.constructorConstructor = constructorConstructor;
this.fieldNamingPolicy = fieldNamingPolicy;
this.excluder = excluder;
}
public boolean excludeField(Field f, boolean serialize) {
return !excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize);
}
private String getFieldName(Field f) {
SerializedName serializedName = f.getAnnotation(SerializedName.class);
return serializedName == null ? fieldNamingPolicy.translateName(f) : serializedName.value();
}
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}
private CustomReflectiveTypeAdapterFactory.BoundField createBoundField(
final Gson context, final Field field, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize) {
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
// special casing primitives here saves ~5% on Android...
return new CustomReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
final TypeAdapter<?> typeAdapter = context.getAdapter(fieldType);
#SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree
#Override void write(JsonWriter writer, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = field.get(value);
TypeAdapter t =
new CustomTypeAdapterRuntimeTypeWrapper(context, this.typeAdapter, fieldType.getType());
t.write(writer, fieldValue);
}
#Override void read(JsonReader reader, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = null;
try {
fieldValue = typeAdapter.read(reader);
} catch (GsonEnumParsinException e){
e.setFieldName(field.getName());
throw e;
}
if (fieldValue != null || !isPrimitive) {
field.set(value, fieldValue);
}
}
};
}
// more copy&paste code follows
The most important part is read method where I catch the exception and add the field name and throw again exception. Note that class CustomTypeAdapterRuntimeTypeWrapper is simply a renamed copy of TypeAdapterRuntimeTypeWrapper in library internals since class is private.
So, main method changes as follows:
Map<Type, InstanceCreator<?>> instanceCreators
= new HashMap<Type, InstanceCreator<?>>();
Excluder excluder = Excluder.DEFAULT;
FieldNamingStrategy fieldNamingPolicy = FieldNamingPolicy.IDENTITY;
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapterFactory(new CustomReflectiveTypeAdapterFactory(new ConstructorConstructor(instanceCreators), fieldNamingPolicy, excluder));
gb.registerTypeAdapterFactory(CUSTOM_ENUM_FACTORY);
Gson g = gb.create();
and now you have this stacktrace (changes to exception are so simple that I omitted them):
Exception in thread "main" GsonEnumParsinException [notFoundEnumValue=RED, enumName=stackoverflow.questions.q16715117.Colour, fieldName=colour]
at stackoverflow.questions.q16715117.Q16715117$CustomEnumTypeAdapter.read(Q16715117.java:90)
at stackoverflow.questions.q16715117.Q16715117$CustomEnumTypeAdapter.read(Q16715117.java:1)
at stackoverflow.questions.q16715117.CustomReflectiveTypeAdapterFactory$1.read(CustomReflectiveTypeAdapterFactory.java:79)
at stackoverflow.questions.q16715117.CustomReflectiveTypeAdapterFactory$Adapter.read(CustomReflectiveTypeAdapterFactory.java:162)
at com.google.gson.Gson.fromJson(Gson.java:803)
at com.google.gson.Gson.fromJson(Gson.java:768)
at com.google.gson.Gson.fromJson(Gson.java:717)
at com.google.gson.Gson.fromJson(Gson.java:689)
at stackoverflow.questions.q16715117.Q16715117.main(Q16715117.java:35)
Of course this solution comes at some costs.
First off all, you have to copy some private/final classes and do your changes. If library get updated, you have to check again your code (a fork of source code would be the same, but at least you do not have to copy all that code).
If you customize field exclusion strategy, constructors or field naming policies you have to replicate them into the CustomReflectiveTypeAdapterFactory since I do not find any possibility to pass them from the builder.

Categories

Resources