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

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.

Related

Gson Failing to call custom Serializer

I've been trying to follow the advice given here to turn off scientific notation on numeric values represented in Json. The problem I've got is that my custom Serializer is never called.
I've tried different variations of the code and have eventually ended up with:
public class TestExternaliser {
static class SpecialSerializer implements JsonSerializer<Object> {
#Override
public JsonElement serialize(Object x,
Type type,
JsonSerializationContext jsonSerializationContext) {
return new JsonPrimitive("xxx");
}
}
public static void main(String... args) {
JsonObject root = new JsonObject();
root.addProperty("String", "String");
root.addProperty("Num", Integer.valueOf(123));
root.addProperty("Bool", Boolean.TRUE);
Gson gson = new GsonBuilder()
.registerTypeHierarchyAdapter(Object.class, new SpecialSerializer())
.setPrettyPrinting()
.create();
System.out.println(gson.toJson(root));
}
}
If I've understood the API correctly then this code use the custom serialisation for all values so it should generate "xxx" for all values, but what I keep getting is:
{
"String": "String",
"Num": 123,
"Bool": true
}
What's going wrong?
What's going wrong?
Nothing wrong because of the limitations Gson has by design: Object and JsonElement type adapter hierarchies cannot be overridden.
Here is the test covering all four object/number hierarchy and value/JSON tree pairs:
public final class LimitationsTest {
private static final JsonSerializer<Object> defaultJsonSerializer = (src, typeOfSrc, context) -> new JsonPrimitive("xxx");
private static final Gson objectDefaultsGson = new GsonBuilder()
.registerTypeHierarchyAdapter(Object.class, defaultJsonSerializer)
.create();
private static final Gson numberDefaultsGson = new GsonBuilder()
.registerTypeHierarchyAdapter(Number.class, defaultJsonSerializer)
.create();
private static final class Value {
#SerializedName("String")
private String string;
#SerializedName("Num")
private Number num;
#SerializedName("Bool")
private Boolean bool;
}
private static final Object object;
private static final JsonElement jsonElement;
static {
final Value newObject = new Value();
newObject.string = "String";
newObject.num = 123;
newObject.bool = Boolean.TRUE;
object = newObject;
final JsonObject newJsonElement = new JsonObject();
newJsonElement.addProperty("String", "String");
newJsonElement.addProperty("Num", 123);
newJsonElement.addProperty("Bool", Boolean.TRUE);
jsonElement = newJsonElement;
}
#Test
public void testObjectObject() {
Assertions.assertEquals("\"xxx\"", objectDefaultsGson.toJson(object));
}
#Test
public void testObjectJsonElement() {
Assertions.assertEquals("{\"String\":\"String\",\"Num\":123,\"Bool\":true}", objectDefaultsGson.toJson(jsonElement));
}
#Test
public void testNumberObject() {
Assertions.assertEquals("{\"String\":\"String\",\"Num\":\"xxx\",\"Bool\":true}", numberDefaultsGson.toJson(object));
}
#Test
public void testNumberJsonElement() {
Assertions.assertEquals("{\"String\":\"String\",\"Num\":123,\"Bool\":true}", numberDefaultsGson.toJson(jsonElement));
}
}
In short JsonElements are considered already-serialized, so what you're looking for is hidden in testNumberObject: define Number as a superclass (or Float/Double to be most precise), and serialize an object containing fields, not JsonElement. If you must use JsonElement, then put a "good-formattible" value right into the Num property (BigDecimal should work just fine).
Update 1.
#Test
public void testNoScientificNotationForJsonElement() {
final JsonObject newJsonElement = new JsonObject();
newJsonElement.addProperty("a", new BigDecimal(new BigDecimal("1E+10").toPlainString()));
newJsonElement.addProperty("b", new BigDecimal("1E+10") {
#Override
public String toString() {
return toPlainString();
}
});
final Gson gson = new Gson();
Assertions.assertEquals("{\"a\":10000000000,\"b\":10000000000}", gson.toJson(newJsonElement));
}
In playwright, a microsoft library, and rust-rcon library, something similar happened to me. I leave you link.
This error occurs because you have installed jdk 11 or upper and a gson prior to 2.8.6
https://github.com/microsoft/playwright-java/issues/245#issuecomment-775351308
https://github.com/MrGraversen/rust-rcon/pull/2#event-4300625968
The solution was to go to the latest version of gson, although the version was the one they used, I added it to my POM to force maven to make the rest of the dependencies use the latest version. Try to see and tell me!
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
Try this solution :D

Deserializing arbitrary JSON in Java with Gson and respecting integers [duplicate]

This question already has answers here:
How to prevent Gson from expressing integers as floats
(10 answers)
Closed 4 years ago.
I have some JSON string snippets which could look like this:
{label: "My Label"}
{maxlength: 5}
{contact: {name: "John", "age": 5, children: [{"name": "Mary"]}}
etc, i.e. it could be any JSON object with any key names or value types.
Right now I am deserializing doing something pretty simple like this:
final Gson gson = new Gson();
Object newValue = gson.fromJson(stringValue, Object.class);
And this is working for 99% of the use cases. But as is mentioned here, it is converting any integers to doubles.
I'm fine registering a type adapter as is recommended elsewhere. So I wrote the following:
final Gson gson = new GsonBuilder()
.registerTypeAdapter(Object.class, new DoubleToInt())
.create();
Object newValue = gson.fromJson(stringValue, Object.class);
private static class DoubleToInt implements JsonDeserializer<Object>{
#Override
public Object deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// do my custom stuff here
return json;
}
}
But this isn't working at all. It's like the type adapter is not even getting registered because breakpoints never even hit in the deserialize method.
As the post you link suggested, you should create custom class, so I did and it's working correctly:
public class Test {
public static void main(String[] args) {
final Gson gson = new GsonBuilder()
.registerTypeAdapter(MyClass.class, new DoubleToInt())
.create();
String stringValue = "{contact: {name: \"John\", \"age\": 5, children: [{\"name\": \"Mary\"}]}}";
MyClass newValue = gson.fromJson(stringValue, MyClass.class);
System.out.println(newValue.toString());
}
private static class DoubleToInt implements JsonDeserializer<MyClass> {
#Override
public MyClass deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// do my custom stuff here
return new MyClass(json);
}
}
}
class MyClass {
private JsonElement element;
MyClass(JsonElement element) {
this.element = element;
}
#Override
public String toString() {
return element.toString();
}
}
In the post you linked, they suggested using a custom class in order to tell what data types you should use instead of using Object.class. Have you tried doing that?
class CustomClass{
String label;
int maxLength;
...
}
Object newValue = gson.fromJson(stringValue, CustomClass.class);

Gson: JSON deserialization issue

I'm trying to deserialize a JSON file using custom deserialization from Gson but I guess I'm failing to do it, nothing is being deserialized. Here's my code
Please also guide me if I'm using incorrect return type. I'm not sure if I'm doing it correctly as well.
GsonHelper.java
public <T> T ProcessData(Class<T> ClassType, String Data)
{
Gson gson = new GsonBuilder().registerTypeAdapter(ClassType , new
JsonDeserializerHelper(ClassType)).create();
return gson.fromJson(Data,ClassType);
}
JsonDeserializerHelper.java
public class JsonDeserializerHelper implements JsonDeserializer {
private Class<?> InstantiatedClass;
private static final Logger logger =
Logger.getLogger(JsonDeserializerHelper.class.getName());
public JsonDeserializerHelper(Class instantiatedClass) {
this.InstantiatedClass = instantiatedClass;
}
#Override
public Object deserialize(JsonElement json, Type type,
JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = (JsonObject) json;
// Implementation here
}
return null;
}
}
Also whenever I try to return a generic type of T, I can't .
What I try to achieve is to have a generic function that returns any class based on the instantiated class in it's private class.
The variable that is consuming the functions afterall
Class<?> AnonymousClass = Class.forName(ClassName);
ProcessClassHelper helperClass = new ProcessClassHelper();
Object ParsedData = (AccountInfo)helperClass.ProcessData(AnonymousClass,json);
ArrayList<AccountInfo> test = (ArrayList<AccountInfo>) ParsedData;
for (AccountInfo acc : test)
{
logger.info("First Name: " + acc.getFirstName());
}
Also the last logger outputs nothing.

How do I use a custom Json Serializer inside another serializer

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.

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