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);
Related
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
I have a list(list contains the requiredFields and this list can get the dynamic data from text file) and I have api response jsonData.
Now, I need to extract the data from api(jsonData) response, only the required fields(what list contained fields). All this need to be done using gson serializer
public class EDSJsonSerializer implements JsonDeserializer {
final list<String>; // list can be populated by reading data from text
file
//ex: list<Strin> is : [ab,bc]
#Override
public JsonElement deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
jsonElement => {"ab":"234234","bc":"wrwerewe","ww":"345fsd","456":"dfgdfg"}
final Map map = new HahMap();
map should contain only 2 elements {"ab":"234234","bc":"wrwerewe"}
map can be populated with list above given as keys and values from json passed
}
}
final String json = ""; // json is the api response string
final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Map.class, new EDSJsonSerializer());
final Gson gson = builder.create();
final String map = gson.toJson(json);
it is not working as expected and not throwing any error/exception.
Please help me on this
Thanks,
Syamala.
Firstly 456 is not a valid Java class field name. It might be one reason of your problems even you might never use it from Json.
Secondly you migh better use ExclusionStrategy to decide which fields to de- & serialize.
In your case something like:
#RequiredArgsConstructor
public class ExcludeUnlistedFields implements ExclusionStrategy {
#NonNull
private Set<String> fieldsToInclude;
#Override
public boolean shouldSkipField(FieldAttributes f) {
// if you need to restrict to specific a class/classes
// add the checks here also
return ! fieldsToInclude.contains(f.getName());
}
#Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}
and use it like:
Set<String> fieldsToInclude = new HashSet<>(Arrays.asList("ab", "bc"));
ExclusionStrategy es = new ExcludeUnlistedFields(fieldsToInclude);
Gson gson = new GsonBuilder().setPrettyPrinting()
.addDeserializationExclusionStrategy(es).create();
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.
Whenever I get error, the error body is as follows:
[
{
"errorCode": 10001,
"resource": null,
"resourceId": null,
"field": null,
"parameter": null,
"header": null,
"allowedValues": null,
"maxLength": null,
"minLength": null
}
]
The error body is an array. I have different bodies for success of many API methods, but the error array response is standardized. I tried doing many things
making wrapper class with generic type success response and array of error response and made deserializer for that, but I can't deserialize from type variable and from paramaterized class.
made a ErrorDeserializer but I have no idea how can I make Retrofit use it for error responses.
I could definitely just serialize raw string everytime on every callback for all my api methods, but I have so many of them, I need generalized solution. If I didn't explain myself properly, please ask.
I'll add examples of what I tried (they will be incomplete however):
Response wrap class:
public class ResponseWrap<T> {
#Nullable
private final T response;
#Nullable
private final List<ErrorResponse> errorResponses;
public ResponseWrap(#Nullable T response, #Nullable List<ErrorResponse> errorResponses) {
this.response = response;
this.errorResponses = errorResponses;
}
}
Error response class:
public class ErrorResponse {
private int errorCode;
private String resource;
private String resourceId;
private String field;
private String parameter;
private String header;
private String allowedValues;
private int maxLength;
private int minLength;
// getters and setters
}
Error deserializer:
public class ErrorDeserializer implements JsonDeserializer<ArrayList<ErrorResponse>> {
#Override
public ArrayList<ErrorResponse> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
Gson gson = new Gson();
Type listType = new TypeToken<ArrayList<ErrorResponse>>(){}.getType();
ArrayList<ErrorResponse> list = new Gson().fromJson(json, listType);
final JsonArray jsonArray = json.getAsJsonArray();
for (int i = 0; i < jsonArray.size(); i++) {
ErrorResponse error = new ErrorResponse();
JsonObject jsonObject = jsonArray.get(i).getAsJsonObject();
error.setErrorCode(jsonObject.get("errorCode").getAsInt());
error.setResource(jsonObject.get("resource").getAsString());
error.setResourceId(jsonObject.get("resourceId").getAsString());
error.setField(jsonObject.get("field").getAsString());
error.setParameter(jsonObject.get("parameter").getAsString());
error.setHeader(jsonObject.get("header").getAsString());
error.setAllowedValues(jsonObject.get("allowedValues").getAsString());
error.setMaxLength(jsonObject.get("maxLength").getAsInt());
error.setMinLength(jsonObject.get("minLength").getAsInt());
list.add(error);
}
return list;
}
}
Response wrap deserializer - it's not working, 2 errors:
List error = new Gson().fromJson(jsonObject.getAsJsonObject("error"), ArrayList.class); // Can't select from parameterized class
T success = new Gson().fromJson(jsonObject, T.class); // Can't select from type variable
public class ResponseWrapDeserializer<T> implements JsonDeserializer<ResponseWrap<T>> {
#Override
public ResponseWrap<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// Get JsonObject
final JsonObject jsonObject = json.getAsJsonObject();
if (jsonObject.has("error")) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(typeOfT, new ErrorDeserializer())
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.create();
List<ErrorResponse> error = new Gson().fromJson(jsonObject.getAsJsonObject("error"), ArrayList<ErrorResponse>.class);
return new ResponseWrap<T>(null, error);
} else {
T success = new Gson().fromJson(jsonObject, T.class);
return new ResponseWrap<T>(success, null);
}
}
}
The idea was to use them all like this:
#POST("Login")
Call<ResponseWrap<AccessTokenResponse>> Login(#Body LoginRequest request);
But I can't because of above mentioned reasons.
The question is: How to process error responses in a generic way that are in an array using Retrofit2?
You cannot write T.class -- this is illegal in Java. In order to overcome this limitation you must either pas a Type instance yourself somehow or resolve generic types parameters from what Gson gives you. In the first case you'd need dozen JSON deserializers to bind various ResponseWrap<T> parametization; whilst in the second case can simply resolve the actual type parameter yourself. At the call site you can use TypeTokens -- a special Gson mechanism to define a type parameter via a type parameterization. Also note that you don't have to instantiate internal Gson instances: this might be relatively expensive (especially in sequence) and disrespect the Gson configuration the current deserializer is bound for - use JsonDeserializationContext since it can give you all you need (except downstream type adapters).
The following JSON deserializer uses the second approach as I find it more convenient.
final class ResponseWrapJsonDeserializer<T>
implements JsonDeserializer<ResponseWrap<T>> {
// This deserializer holds no state, so we can hide its instantiation details away
private static final JsonDeserializer<ResponseWrap<Object>> responseWrapJsonDeserializer = new ResponseWrapJsonDeserializer<>();
// Type instances from TypeToken seems to be fully immutable and can be treated as value types, thus we can make them static final to re-use (it's safe)
private static final Type errorResponseListType = new TypeToken<List<ErrorResponse>>() {
}.getType();
private ResponseWrapJsonDeserializer() {
}
// Just cheating the call site: we always return the same instance if the call site requests for a specially typed deserializer (it's always the same instance however, this is just how Java generics work)
static <T> JsonDeserializer<ResponseWrap<T>> getResponseWrapJsonDeserializer() {
#SuppressWarnings({ "rawtypes", "unchecked" })
final JsonDeserializer<ResponseWrap<T>> cast = (JsonDeserializer) responseWrapJsonDeserializer;
return cast;
}
#Override
public ResponseWrap<T> deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
// Checking if jsonElement looks like an error (I'm not sure if it's possible to check HTTP statuses delegating them to request/response converters in Retrofit)
if ( isError(jsonElement) ) {
final List<ErrorResponse> errorResponses = context.deserialize(jsonElement, errorResponseListType);
return new ResponseWrap<>(null, errorResponses);
}
// If it does not look an error, then:
// * resolve what's the actual T in the given ResponseWrap<T>
// * deserialize the JSON tree as an instance of T -- it's like we're stripping the wrapper and then instantiate the wrap due to our rules
final T response = context.deserialize(jsonElement, resolveTypeParameter0(type));
return new ResponseWrap<>(response, null);
}
private static Type resolveTypeParameter0(final Type type) {
// The given type does not have parameterization?
if ( !(type instanceof ParameterizedType) ) {
// Then it's raw, simply <Object> or <?>
return Object.class;
}
// If it's parameterized, let's take it's first parameter as ResponseWrap is known to a have a single type parameter only
return ((ParameterizedType) type).getActualTypeArguments()[0];
}
// Some AI party here, he-he
private static boolean isError(final JsonElement jsonElement) {
if ( !jsonElement.isJsonArray() ) {
return false;
}
final JsonArray jsonArray = jsonElement.getAsJsonArray();
for ( final JsonElement innerJsonElement : jsonArray ) {
if ( !innerJsonElement.isJsonObject() ) {
return false;
}
final JsonObject innerJsonObject = innerJsonElement.getAsJsonObject();
final boolean looksLikeErrorObject = innerJsonObject.has("errorCode");
if ( !looksLikeErrorObject ) {
return false;
}
}
return true;
}
}
Next, register the deserializer for your Gson instance:
private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(ResponseWrap.class, getResponseWrapJsonDeserializer())
.create();
And test it with
success.json
{
"foo": [1, 2, 3]
}
failure.json
[
{"errorCode": 10001},
{"errorCode": 10002}
]
// It's a constant
// Also, ResponseWrap<Map<String,List<Integer>>>.class is illegal in Java
private static final Type type = new TypeToken<ResponseWrap<Map<String, List<Integer>>>>() {
}.getType();
public static void main(String... args)
throws IOException {
final String successJson = getPackageResourceString(Q43525433.class, "success.json");
final String failureJson = getPackageResourceString(Q43525433.class, "failure.json");
final ResponseWrap<Map<String, List<Integer>>> success = gson.fromJson(successJson, type);
final ResponseWrap<Map<String, List<Integer>>> failure = gson.fromJson(failureJson, type);
System.out.println("SUCCESS: " + success.response);
for ( final ErrorResponse response : failure.errorResponses ) {
System.out.println("FAILURE: " + response.errorCode);
}
}
The output:
SUCCESS: {foo=[1, 2, 3]}
FAILURE: 10001
FAILURE: 10002
And yes, don't forget to add gson to Retrofit using GsonConverterFactory.create(gson).
Also, you might be interested in Json response parser for Array or Object that describe the almost the same issue but from another perspective.
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.