How do I use a custom Json Serializer inside another serializer - java

I have some Objects
public class MyObject {
private String name;
private String city;
public MyObject(String n, String c) {
name=n; city=c;}
public String getName() {return name;}
public String getCity() {return city;}
public void setName(String n) {name=n;}
public void setCity(String c) {city =c;}
}
And i have a custom serializer:
public class MySerializer implements JsonSerializer<MyObject> {
public JsonElement serialize(final MyObject myobj, final Type type, JsonSerializationContext context) {
JsonObject result = new JsonObject();
result.add("id", new JsonPrimitive(myobj.getName()));
return result;
}
}
Basically i just want to serilize only 1 of the 2 fields. this works great when do something like:
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(MyObject.class, new MySerialize());
Gson gson = builder.create();
System.out.println(gson.toJson(new MyObject("test","something"));
however it gets a bit more complicated (and here is my question) when i have another object which is made up of "MyObject"s. How can i get the correct serializer to only serialize the one field of MyObject.
so i have another class:
public class SomeObject {
private String id;
private MyObject foo;
private MyObject bar;
...
}
and i have a custom serializer:
public JsonElement serialize(final SomeObject something, final Type type, JsonSerializationContext context) {
JsonObject result = new JsonObject();
Gson gson = new Gson();
result.add("id", new JsonPrimitive(something.getId()));
//here i need help
result.add("myobject1", new JsonPrimitive(gson.toJson(something.getFoo())));
return result;
}
I'm not sure if its best practice to create the GsonBuilder for "MyObject" inside the custom serializer for SomeObject is it?
ive tried something like:
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(MyObject.class, new MySerialize());
builder.registerTypeAdapter(SomeObject.class, new SomeObjectSerializer());
Gson gson = builder.create();
System.out.println(gson.toJson(new SomeObject("id",new MyObject("test","something"),new MyObject("test2,"barrrrr"));
and i would exepct "{"id":"id","foo"{"id":"test"},"bar":{"id:"test2"}}
but that is not the case. bascially i want just the first field in a custom object whcih i have a seralizer for, but do i need to build that serializer inside another objects custom serializer? seems wrong, dunno why.

Note how you have access to the JsonSerializationContext in your custom JsonSerializer classes. You can call JsonSerializationContext#serialize(Object) and Gson will use a registered or default TypeAdapter to serialize that object and return a JsonElement which you can add to the outer JsonElement.

Related

Convert String to ArrayList<String> using GSON

I am trying to deserialize a JSON data to a POJO.
The issue is that the list object is coming as a string, and gson gives an IllegalStateExceptioState. How can I parse the string as a list to an ArrayList using gson?
JSON DATA
{
"report_id":1943,
"history_id":3302654,
"project_id":null,
"owner_emails":"[\"abcd#xyz.com\"]",
"message":"Array\n(\n [name] => SOMENAME\n [age] => 36\n [gender] => male\n)\n"
}
POJO:
public class EventData {
private static Gson gson = new Gson();
#SerializedName("report_id")
public String reportID;
#SerializedName("history_id")
public String historyID;
#SerializedName("project_id")
public String projectID;
#SerializedName("owner_emails")
public ArrayList<String> ownerEmails = new ArrayList<String>();
#SerializedName("message")
public String message;
#SerializedName("title")
public String title;
public CrawlerNotifiedEventData(){
this.projectID = "Undefined";
this.reportID = "Undefined";
this.historyID = "Undefined";
this.title = "";
}
public String toJson(boolean base64Encode) throws java.io.UnsupportedEncodingException{
String json = gson.toJson(this, CrawlerNotifiedEventData.class);
if(base64Encode)
return Base64.getEncoder().encodeToString(json.getBytes("UTF8"));
return json;
}
public String toJson() throws java.io.UnsupportedEncodingException{
return this.toJson(false);
}
public static EventData builder(String json){
return gson.fromJson(json, EventData.class);
}
}
Deserialization:
EventData eventData = EventData.builder(json);
While deserializing i get the following error
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 252 path $.owner_emails
Boxing structured data in a string where it is unnecessary is a very common design issue across different serialization approaches. Fortunately, Gson can deal with fields like owner_emails (but not message of course).
Merely create a type adapter factory than can create a type adapter for a particular type by substituting the original one and doing a bit of more work. The adapter is supposed to read the payload as string and delegate the string deserialization to the type adapter it substitutes.
public final class JsonStringBoxTypeAdapterFactory
implements TypeAdapterFactory {
private JsonStringBoxTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final TypeAdapter<T> adapter = gson.getAdapter(typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value) {
throw new UnsupportedOperationException(); // TODO
}
#Override
public T read(final JsonReader in)
throws IOException {
return adapter.fromJson(in.nextString());
}
};
}
}
#AllArgsConstructor
#ToString
#EqualsAndHashCode
final class EventData {
#SerializedName("owner_emails")
#JsonAdapter(JsonStringBoxTypeAdapterFactory.class)
List<String> ownerEmails;
}
The unit test below will be green:
final EventData eventData = gson.fromJson(json, EventData.class);
Assertions.assertEquals(new EventData(ImmutableList.of("abcd#xyz.com")), eventData);
That's it.
"owner_emails" is curently a string as follows
"owner_emails":"[\"abcd#xyz.com\"]"
It should be
"owner_emails": ["abcd#xyz.com"]
to be considered as array. You can manually remove the quotes and parse it.
Or you can parse it using JsonElement in Gson
You can use ObjectMapper from jackson library for this conversion.
Sample code of conversion::
public <T> T mapResource(Object resource, Class<T> clazz) {
try {
return objectMapper.readValue(objectMapper.writeValueAsString(resource), clazz);
} catch (IOException ex) {
throw new Exception();
}
}
Modify the model for a list like::
public class Reportdata{
private List<String> owner_emails = new ArrayList();
#JsonDeserialize(contentAs = CustomClass.class)
private List<CustomClass> customClassList = new ArrayList();
....// setter and getter
}
In addition to this, while creating the ObjectMapper object you can pass or register the module/ your custom module for deserialization in object like below.
objectMapper.setDefaultPropertyInclusion(Include.NON_EMPTY);
objectMapper.disable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
objectMapper.registerModule(new JavaTimeModule());

How to read JSON String attribute into custom class object using Gson?

When reading a JSON :
{"field":"value"}
into a String field :
public class Test {
private String field;
}
using Gson.fromJson it works as intended and the member String field gets the value "value".
My question is, is there a way to read the same JSON into a custom class so that the custom class object can be constructed with the String value? e.g.
public class Test {
private MyField<String> field;
}
public class MyField<T> {
private T value;
public MyField(T v) {
value = v;
}
}
The reason being the String class is final and cannot be extended, yet I don't want the JSON to be changed into this :
{"field":{"value":"value"}}
If there is a way to extend the String class, it is the best. Otherwise, will need a way for Gson to read string into a custom class that can be constructed by string. Something to do with writing a custom TypeAdapter?
You can use custom JsonDeserializer, JsonSerializer. Here is simple demo version:
static class MyFieldAsValueTypeAdapter<T> implements
JsonDeserializer<MyField<T>>, JsonSerializer<MyField<T>> {
private Gson gson = new Gson();
#Override
public MyField<T> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException {
JsonObject obj = new JsonObject();
obj.add("value", json);
return gson.fromJson(obj, typeOfT);
}
#Override
public JsonElement serialize(MyField<T> src, Type typeOfSrc,
JsonSerializationContext context) {
return context.serialize(src.getValue());
}
}
public static void main(String[] args) {
GsonBuilder b = new GsonBuilder();
b.registerTypeAdapter(MyField.class , new MyFieldAsValueTypeAdapter());
Gson gson = b.create();
String json = "{\"field\":\"value1\"}";
Test test = gson.fromJson(json, Test.class);
}
Be careful with internal Gson gson = new Gson(). If you have some other setup, you will need to register it on internal version or pass default MyField deserializer/serializer to your custom implementation.

Java gson replace member object when serialized

I'm trying to design a flatter structure for the output JSON file. How do I make it so that when I use gson.toJson() the member object will be serialized using the object's ID instead of serializing the whole thing?
For example:
class Foo {
public int id;
}
class Bar {
public Foo foo = new Foo();
public String data = "something";
}
I want gson.toJson(bar) to output something like this: {"foo": 1029, "data":"something"}.
Maybe something like this:
public class FooBarAdapter implements JsonSerializer<Bar> {
#Override
public JsonElement serialize(Bar bar, Type typeOfObj,
JsonSerializationContext context)
{
JsonObject obj = new JsonObject();
obj.addProperty("foo", bar.foo.id);
obj.addProperty("data", bar.data);
return obj;
}
}
and then when you want to serialize it:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Bar.class, new FooBarAdapter());
return gsonBuilder.create().toJson(bar);

Gson - Deserialize objects by specifying class, not parametrization

I have the following classes:
public class Top {
private String key;
...
}
public class A extends Top {
private String aValue;
...
}
public class Complex {
private String field;
private List<Top> objects;
}
I want to deserialize a json String into a "Complex" class and specify that "objects" elements are of type "A".
I have tried 2 methods:
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(A.class, new InstanceCreator<A>() {
#Override
public A createInstance(Type arg0) {
return new A();
}
}) //method 1
.registerTypeHierarchyAdapter(A.class, new My_A_Adapter()) //method 2
.create();
Complex complexObject = gson.fromJson(json, Complex.class);
A = (A) complexObject.getObjects().get(0); // This throws ClassCastException
But the type of complexObject.getObjects().get(0) is "Top" so i cannot cast it to "A".
I do not want to parameterize the Complex class, (for ex. Complex) because i want to add more collections of generic objects in time...
What solution do I have ?

How to serialize a class with an interface?

I have never done much with serialization, but am trying to use Google's gson to serialize a Java object to a file. Here is an example of my issue:
public interface Animal {
public String getName();
}
public class Cat implements Animal {
private String mName = "Cat";
private String mHabbit = "Playing with yarn";
public String getName() {
return mName;
}
public void setName(String pName) {
mName = pName;
}
public String getHabbit() {
return mHabbit;
}
public void setHabbit(String pHabbit) {
mHabbit = pHabbit;
}
}
public class Exhibit {
private String mDescription;
private Animal mAnimal;
public Exhibit() {
mDescription = "This is a public exhibit.";
}
public String getDescription() {
return mDescription;
}
public void setDescription(String pDescription) {
mDescription = pDescription;
}
public Animal getAnimal() {
return mAnimal;
}
public void setAnimal(Animal pAnimal) {
mAnimal = pAnimal;
}
}
public class GsonTest {
public static void main(String[] argv) {
Exhibit exhibit = new Exhibit();
exhibit.setAnimal(new Cat());
Gson gson = new Gson();
String jsonString = gson.toJson(exhibit);
System.out.println(jsonString);
Exhibit deserializedExhibit = gson.fromJson(jsonString, Exhibit.class);
System.out.println(deserializedExhibit);
}
}
So this serializes nicely -- but understandably drops the type information on the Animal:
{"mDescription":"This is a public exhibit.","mAnimal":{"mName":"Cat","mHabbit":"Playing with yarn"}}
This causes real problems for deserialization, though:
Exception in thread "main" java.lang.RuntimeException: No-args constructor for interface com.atg.lp.gson.Animal does not exist. Register an InstanceCreator with Gson for this type to fix this problem.
I get why this is happening, but am having trouble figuring out the proper pattern for dealing with this. I did look in the guide but it didn't address this directly.
Here is a generic solution that works for all cases where only interface is known statically.
Create serialiser/deserialiser:
final class InterfaceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T> {
public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
final JsonObject wrapper = new JsonObject();
wrapper.addProperty("type", object.getClass().getName());
wrapper.add("data", context.serialize(object));
return wrapper;
}
public T deserialize(JsonElement elem, Type interfaceType, JsonDeserializationContext context) throws JsonParseException {
final JsonObject wrapper = (JsonObject) elem;
final JsonElement typeName = get(wrapper, "type");
final JsonElement data = get(wrapper, "data");
final Type actualType = typeForName(typeName);
return context.deserialize(data, actualType);
}
private Type typeForName(final JsonElement typeElem) {
try {
return Class.forName(typeElem.getAsString());
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
private JsonElement get(final JsonObject wrapper, String memberName) {
final JsonElement elem = wrapper.get(memberName);
if (elem == null) throw new JsonParseException("no '" + memberName + "' member found in what was expected to be an interface wrapper");
return elem;
}
}
make Gson use it for the interface type of your choice:
Gson gson = new GsonBuilder().registerTypeAdapter(Animal.class, new InterfaceAdapter<Animal>())
.create();
Put the animal as transient, it will then not be serialized.
Or you can serialize it yourself by implementing defaultWriteObject(...) and defaultReadObject(...) (I think thats what they were called...)
EDIT See the part about "Writing an Instance Creator" here.
Gson cant deserialize an interface since it doesnt know which implementing class will be used, so you need to provide an instance creator for your Animal and set a default or similar.
#Maciek solution works perfect if the declared type of the member variable is the interface / abstract class. It won't work if the declared type is sub-class / sub-interface / sub-abstract class unless we register them all through registerTypeAdapter(). We can avoid registering one by one with the use of registerTypeHierarchyAdapter, but I realize that it will cause StackOverflowError because of the infinite loop. (Please read reference section below)
In short, my workaround solution looks a bit senseless but it works without StackOverflowError.
#Override
public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
final JsonObject wrapper = new JsonObject();
wrapper.addProperty("type", object.getClass().getName());
wrapper.add("data", new Gson().toJsonTree(object));
return wrapper;
}
I used another new Gson instance of work as the default serializer / deserializer to avoid infinite loop. The drawback of this solution is you will also lose other TypeAdapter as well, if you have custom serialization for another type and it appears in the object, it will simply fail.
Still, I am hoping for a better solution.
Reference
According to Gson 2.3.1 documentation for JsonSerializationContext and JsonDeserializationContext
Invokes default serialization on the specified object passing the specific type information. It should never be invoked on the element received as a parameter of the JsonSerializer.serialize(Object, Type, JsonSerializationContext) method. Doing so will result in an infinite loop since Gson will in-turn call the custom serializer again.
and
Invokes default deserialization on the specified object. It should never be invoked on the element received as a parameter of the JsonDeserializer.deserialize(JsonElement, Type, JsonDeserializationContext) method. Doing so will result in an infinite loop since Gson will in-turn call the custom deserializer again.
This concludes that below implementation will cause infinite loop and cause StackOverflowError eventually.
#Override
public JsonElement serialize(Animal src, Type typeOfSrc,
JsonSerializationContext context) {
return context.serialize(src);
}
I had the same problem, except my interface was of primitive type (CharSequence) and not JsonObject:
if (elem instanceof JsonPrimitive){
JsonPrimitive primitiveObject = (JsonPrimitive) elem;
Type primitiveType =
primitiveObject.isBoolean() ?Boolean.class :
primitiveObject.isNumber() ? Number.class :
primitiveObject.isString() ? String.class :
String.class;
return context.deserialize(primitiveObject, primitiveType);
}
if (elem instanceof JsonObject){
JsonObject wrapper = (JsonObject) elem;
final JsonElement typeName = get(wrapper, "type");
final JsonElement data = get(wrapper, "data");
final Type actualType = typeForName(typeName);
return context.deserialize(data, actualType);
}

Categories

Resources