Consider this simple Json:
{
"test": [
0,
3
]
}
Now I want to deserialize it in a simple int array so for that I use a custom deserializer:
class ArrayDeserializer implements JsonDeserializer<int[]> {
#Override
public int[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return context.deserialize(json.getAsJsonObject().getAsJsonArray("test"), int[].class);
}
}
and then:
Gson gson = new GsonBuilder().registerTypeAdapter(int[].class, new ArrayDeserializer()).create();
int[] arr = gson.fromJson(json, int[].class);
which throws:
Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Not a JSON Object: [0,3]
However when I do this:
class ArrayDeserializer implements JsonDeserializer<int[]> {
private static final Gson gson = new Gson();
#Override
public int[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return gson.fromJson(json.getAsJsonObject().getAsJsonArray("test"), int[].class);
}
}
it works and I get the expected output. Why?
Let's rewrite your ArrayDeserializer in to an equivalent form but more expressive
class ArrayDeserializer implements JsonDeserializer<int[]> {
#Override
public int[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonArray jsonArray = json.getAsJsonObject().getAsJsonArray("test");
return context.deserialize(jsonArray, int[].class);
}
}
The JsonDeserializationContext#deserialize javadoc states
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.
So, even if it had worked, you'd more than likely have a stackoverflow on your hands.
So why didn't it work?
You called (in pseudo-code)
deserialize {test:[0,3]} as int[]
// --> Gson finds ArrayDeserializer mapped to int[]
take given JSON as an object (OK), extract 'test' as JSON array (OK)
deserialize [0,3] as int[]
// --> Gson finds ArrayDeserializer mapped to int[]
take given JSON as an object (FAIL)
This last time you recurred, the JSON was already in the form of a JSON array but your ArrayDeserializer was expecting a JSON object.
In your second attempt
return gson.fromJson(json.getAsJsonObject().getAsJsonArray("test"), int[].class);
you're again extracting the 'test' JSON array and feeding it to a new Gson instance on which you haven't registered your ArrayDeserializer. In effect, you're invoking
new Gson().fromJson("[0,3]", int[].class);
which is supported out of the box and will return an int[] with the two elements 0 and 3, as expected.
There are simpler solutions.
Define a simple POJO type
class Pojo {
private int[] test;
public int[] getTest() {
return test;
}
public void setTest(int[] test) {
this.test = test;
}
}
and deserialize to it
Pojo pojo = new Gson().fromJson(json, Pojo.class);
int[] arr = pojo.getTest();
Or
Gson gson = new Gson();
JsonArray jsonArray = gson.toJsonTree(json).getAsJsonObject().get("test").getAsJsonArray();
int[] arr = gson.fromJson(jsonArray, int[].class);
Well, looking at your desired version of ArrayDeserializer class:
class ArrayDeserializer implements JsonDeserializer<int[]> {
#Override
public int[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return context.deserialize(json.getAsJsonObject().getAsJsonArray("test"), int[].class);
}
}
Inside deserialize method you are calling again deserialize, so at the second call, you'll not have an array... that is the reason why you have an IllegalStateException.
Your method should create an int[] array (with the appropriate convertions) and then return it. That's why your second version, with gson.fromJson works, because fromJson deserializes to int[]. Maybe you keep using this to do the converting dirt work.
Related
Gson gson = new Gson();
Type type = new TypeToken<HashMap<Rule, HashSet<Data>>>() {
}.getType();
String json = gson.toJson(dailyListSave, type);
HashMap<Rule, HashSet<Data>> list = gson.fromJson(json, type);
Last line from above code is causing exception
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 3 path $.
Not sure why? Really appreciate your help.
I figured out what is wrong.
When using HashMap with a complex Key class, like in the above
case Rule class. you need to enable
enableComplexMapKeySerialization() on a gson object.
List item Data class
is abstract class. When using abstract class to
serialize/deserialize you need to register your custom type adaptor.
Here code snippet
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Data.class, new DataJsonAdapter());
gsonBuilder.enableComplexMapKeySerialization();
gson mGson = gsonBuilder.create();
Here's the DataJsonAdaptor class implementation
Important thing is to set concrete class name during serialize and use it during deserialize. Class name is stored in "type"
public class DataJsonAdapter implements JsonSerializer<Data>, JsonDeserializer<Data> {
#Override
public JsonElement serialize(Data src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
result.add("type", new JsonPrimitive(src.getClass().getSimpleName()));
result.add("properties", context.serialize(src, src.getClass()));
return result;
}
#Override
public Data deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String type = jsonObject.get("type").getAsString();
JsonElement element = jsonObject.get("properties");
try {
return context.deserialize(element,
Class.forName("com.package.name" + type));
} catch (ClassNotFoundException cnfe) {
throw new JsonParseException("Unknown element type: " + type, cnfe);
}
}
}
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);
I want to use GSON to decode an array of maps in which the keys are not Strings. I know that the JSON type does not allow for objects to be used as keys, so I was hoping it is possible to have GSON work recursively to decode Strings.
Java
public class Reader {
static class Key {
int a;
int b;
}
static class Data {
HashMap<Key, Integer> map;
}
public static void read() {
Gson gson = new Gson();
String x = "[{\"map\": { \"{\\\"a\\\": 0, \\\"b\\\": 0}\": 1 }}]";
Data[] y = gson.fromJson(x, Data[].class);
}
}
JSON example
[
{
"map": {
"{\"a\": 0, \"b\": 0}": 1
}
}
]
What I would like to achieve here, is that the string "{\"a\": 0, \"b\": 0}" is decoded by GSON to an object of type Key with both members set to 0. Then, that object could be used to fill out the HashMap of the Data class.
Is this possible to achieve?
You can achieve this with custom JsonDeserializer. With a custom deserializer you can decide howto deserialize this class Key. Implement it somewhere, inline example below:
public JsonDeserializer<Key> keyDs = new JsonDeserializer<Key>() {
private final Gson gson = new Gson();
#Override
public Key deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException {
// This will be valid JSON
String keyJson = json.getAsString();
// use another Gson to parse it,
// otherwise you will have infinite recursion
Key key = gson.fromJson(keyJson, Key.class);
return key;
}
};
Register it with GsonBuilder, create Gson and deserialize:
Data[] mapPojos = new GsonBuilder().registerTypeAdapter(Key.class, ds).create()
.fromJson(x, Data[].class);
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.
I'm trying to serialize my objects back to JSON using Google's Gson using JsonSerializer interface and while deserialization works just fine, serialization doesn't call serialize method.
Serializer / Deserializer classes
public enum JsonParser implements JsonDeserializer<Object>, JsonSerializer<Object> {
LANGUAGE(Language.class) {
#Override
public Language deserialize(JsonElement elem, Type type, JsonDeserializationContext context) {
return Language.valueOf(elem.getAsString());
}
#Override
public JsonElement serialize(Object object, Type type, JsonSerializationContext context) {
return new JsonPrimitive(((Language) object).getCode());
}
},
DATA_TYPE(DataType.class) {
#Override
public DataType deserialize(JsonElement elem, Type type, JsonDeserializationContext context) {
return DataType.getByIdentifier(elem.getAsString());
}
#Override
public JsonElement serialize(Object object, Type type, JsonSerializationContext context) {
System.out.println("test");
return new JsonPrimitive(((DataType) object).getIdentifier());
}
};
private final Class clazz;
JsonParser(Class clazz) {
this.clazz = clazz;
}
public Class getParserClass() {
return clazz;
}
}
And tests:
GsonBuilder gsonBuilder = new GsonBuilder();
for(JsonParser jp : JsonParser.values())
gsonBuilder.registerTypeAdapter(jp.getParserClass(), jp);
Gson gson = gsonBuilder.create();
String json = "{\"type1\":{\"da\":\"Some string\",\"pt_BR\":\"More strings\",\"pl\":\"String 3\",\"eo\":\"String 4\"},\"type2\":{\"pl\":\"String 5\",\"pt_BR\":\"String 6\",\"ru\":\"String 7\"}}";
Map<DataType, Map<Language, String>> map = gson.fromJson(json, new TypeToken<Map<DataType, Map<Language, String>>>(){}.getType());
System.out.println(map);
System.out.println(gson.toJson(map));
While the fromJson() returns correct objects, toJSON() uses default toString() from objects instead of the methods specified in serialize() method.
It seems that serializer is not getting registered for some reason (the test printout doesn't show up).
Thanks for any suggestions.
Gson will use the EnumTypeAdapter to deserialize your enum, since, if I understood correctly the internals of Gson, this type adapter will be called before than the reflective one, which uses instead your serializer stuff.
This question will address you on how you can change the JSON serialization of your enum (using a TypeAdapter)