58

I'm reading a JSON response with Gson, which returns somtimes a NumberFormatException because an expected int value is set to an empty string. Now I'm wondering what's the best way to handle this kind of exception. If the value is an empty string, the deserialization should be 0.

Expected JSON response:

{
   "name" : "Test1",
   "runtime" : 90
}

But sometimes the runtime is an empty string:

{
   "name" : "Test2",
   "runtime" : ""
}

The java class looks like this:

public class Foo
{
    private String name;
    private int runtime;
}

And the deserialization is this:

String input = "{\n" +
               "   \"name\" : \"Test\",\n" +
               "   \"runtime\" : \"\"\n" +
               "}";

Gson gson = new Gson();
Foo foo = gson.fromJson(input, Foo.class);

Which throws a com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: empty String because an empty String is returned instead of an int value.

Is there a way to tell Gson, "if you deserialize the field runtime of the Type Foo and there is a NumberFormatException, just return the default value 0"?

My workaround is to use a String as the Type of the runtime field instead of int, but maybe there is a better way to handle such errors.

Soundlink
  • 3,915
  • 2
  • 28
  • 36

8 Answers8

40

Here is an example that I made for Long type. This is a better option:

public class LongTypeAdapter extends TypeAdapter<Long> {

    @Override
    public Long read(JsonReader reader) throws IOException {
        if (reader.peek() == JsonToken.NULL) {
            reader.nextNull();
            return null;
        }
        String stringValue = reader.nextString();
        try {
            Long value = Long.valueOf(stringValue);
            return value;
        } catch (NumberFormatException e) {
            return null;
        }
    }

    @Override
    public void write(JsonWriter writer, Long value) throws IOException {
        if (value == null) {
            writer.nullValue();
            return;
        }
        writer.value(value);
    }
}

Register an adapter using Gson util:

Gson gson = new GsonBuilder().registerTypeAdapter(Long.class, new LongTypeAdapter()).create();

You can refer to this link for more.

CoolMind
  • 26,736
  • 15
  • 188
  • 224
Manish Khandelwal
  • 2,260
  • 2
  • 15
  • 13
  • How to register this adapter on gson builder? – masterdany88 Dec 22 '14 at 17:59
  • 3
    Gson gson = new GsonBuilder().registerTypeAdapter(Long.class, new LongTypeAdapter()).create(); – Adrian B. Jun 21 '16 at 14:09
  • 2
    Not working . Still i am getting same error java.lang.NumberFormatException: Expected a long but was 16.6 – Tanmay Sahoo Nov 22 '18 at 05:49
  • 3
    For whoever is looking for an answer on this for kotlin, do the same as the top answer but instead of doing `registerTypeAdapter(Long.class.java, adapter)` do `registerTypeAdapter(java.lang.Long::class.java, adapter)` – lucas_sales Mar 13 '19 at 12:11
16

At first, I tried to write a general custom type adaptor for Integer values, to catch the NumberFormatException and return 0, but Gson doesn't allow TypeAdaptors for primitive Types:

java.lang.IllegalArgumentException: Cannot register type adapters for class java.lang.Integer

After that I introduced a new Type FooRuntime for the runtime field, so the Foo class now looks like this:

public class Foo
{
    private String name;
    private FooRuntime runtime;

    public int getRuntime()
    {
        return runtime.getValue();
    }
}

public class FooRuntime
{
    private int value;

    public FooRuntime(int runtime)
    {
        this.value = runtime;
    }

    public int getValue()
    {
        return value;
    }
}

A type adaptor handles the custom deserialization process:

public class FooRuntimeTypeAdapter implements JsonDeserializer<FooRuntime>, JsonSerializer<FooRuntime>
{
    public FooRuntime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
    {
        int runtime;
        try
        {
            runtime = json.getAsInt();
        }
        catch (NumberFormatException e)
        {
            runtime = 0;
        }
        return new FooRuntime(runtime);
    }

    public JsonElement serialize(FooRuntime src, Type typeOfSrc, JsonSerializationContext context)
    {
        return new JsonPrimitive(src.getValue());
    }
}

Now it's necessary to use GsonBuilder to register the type adapter, so an empty string is interpreted as 0 instead of throwing a NumberFormatException.

String input = "{\n" +
               "   \"name\" : \"Test\",\n" +
               "   \"runtime\" : \"\"\n" +
               "}";

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(FooRuntime.class, new FooRuntimeTypeAdapter());
Gson gson = builder.create();
Foo foo = gson.fromJson(input, Foo.class);
Soundlink
  • 3,915
  • 2
  • 28
  • 36
  • 31
    wow, that's a lot of code for something that should be trivial :-s – Somatik May 28 '12 at 17:49
  • 6
    As of version 2.3.1, GSON now does allow you to register type adapters for primitive types. I was able to solve the same problem by registering a `JsonDeserializer` for `Integer.class` that returns 0 in case of a `NumberFormatException`. – Edward Jan 01 '15 at 18:25
15

Quick and easy workaround - Just change your member type field of runtime to String and access it via getter that returns runtime as an int:

public class Foo
{
    private String name;
    private String runtime;

    public int getRuntime(){
        if(runtime == null || runtime.equals("")){
            return 0;
        }
        return Integer.valueOf(trackId);
    }
}

=> no json deserialization neccessary

Lukas Lechner
  • 7,881
  • 7
  • 40
  • 53
6

I've made this TypeAdapter which check for empty strings and return 0

public class IntegerTypeAdapter extends TypeAdapter<Number> {
@Override
public void write(JsonWriter jsonWriter, Number number) throws IOException {
    if (number == null) {
        jsonWriter.nullValue();
        return;
    }
    jsonWriter.value(number);
}

@Override
public Number read(JsonReader jsonReader) throws IOException {
    if (jsonReader.peek() == JsonToken.NULL) {
        jsonReader.nextNull();
        return null;
    }

    try {
        String value = jsonReader.nextString();
        if ("".equals(value)) {
            return 0;
        }
        return Integer.parseInt(value);
    } catch (NumberFormatException e) {
        throw new JsonSyntaxException(e);
    }
}

}

Valdo Raya
  • 140
  • 1
  • 8
5

As stated in another comment, as of GSON 2.3.1 you can register type adapters for primitive types, here is a type adapter that handles int and Integer types, and gracefully defaults to 0 (or null) for strings, booleans and nulls. This will continue to parse strings that have numbers in them like "runtime" : "5".

public static final TypeAdapter<Number> UNRELIABLE_INTEGER = new TypeAdapter<Number>() {
    @Override
    public Number read(JsonReader in) throws IOException {
        JsonToken jsonToken = in.peek();
        switch (jsonToken) {
            case NUMBER:
            case STRING:
                String s = in.nextString();
                try {
                    return Integer.parseInt(s);
                } catch (NumberFormatException ignored) {
                }
                try {
                    return (int)Double.parseDouble(s);
                } catch (NumberFormatException ignored) {
                }
                return null;
            case NULL:
                in.nextNull();
                return null;
            case BOOLEAN:
                in.nextBoolean();
                return null;
            default:
                throw new JsonSyntaxException("Expecting number, got: " + jsonToken);
        }
    }
    @Override
    public void write(JsonWriter out, Number value) throws IOException {
        out.value(value);
    }
};
public static final TypeAdapterFactory UNRELIABLE_INTEGER_FACTORY = TypeAdapters.newFactory(int.class, Integer.class, UNRELIABLE_INTEGER);

You can register it with the following code

Gson gson = new GsonBuilder()
            .registerTypeAdapterFactory(UNRELIABLE_INTEGER_FACTORY)
            .create();

Note that the normal JsonReader.nextInt() that this replaces attempts to call parseInt and parseDouble on the token, so this will replicate internal logic for parsing integers.

seanalltogether
  • 3,542
  • 3
  • 26
  • 24
2

This solution works for Double types. This will only work for non-primitive types:

public class DoubleGsonTypeAdapter implements JsonDeserializer<Double> {

    @Override
    public Double deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
        Double result = null;
        try {
            result = jsonElement.getAsDouble();
        } catch (NumberFormatException e) {
            return result;
        }
        return result;
    }
}

Model:

@SerializedName("rateOfInterest")
public Double rateOfInterest;
@SerializedName("repaymentTenure")
public Double repaymentTenure;
@SerializedName("emiAmount")
public Double emiAmount;

Retrofit client:

Gson gson = new GsonBuilder().registerTypeAdapter(Double.class, new DoubleGsonTypeAdapter()) .create();

Retrofit retrofit = new Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl(API_BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
Prateek Bhuwania
  • 755
  • 1
  • 8
  • 19
2

It might help you to always assume a default value of 0 for the field runtime in case of a NumberFormatException, since it can be the only source of error.

Milad Naseri
  • 4,053
  • 1
  • 27
  • 39
  • 1
    Please explain how you would tell the Gson object to do that. – Soundlink Jan 14 '12 at 21:51
  • You do that using a type adapter. See [here](http://grepcode.com/file/repository.jboss.org/maven2/org.jbpm.jbpm3/gwt-console/1.0.0.Beta2/com/google/gson/JsonDeserializer.java#JsonDeserializer) and [here](http://grepcode.com/file/repository.jboss.org/maven2/org.jbpm.jbpm3/gwt-console/1.0.0.Beta2/com/google/gson/DefaultTypeAdapters.java). – Milad Naseri Jan 14 '12 at 21:58
  • Of course, a more fix-and-go solution would be to look for invalid input in the JSON string using regex and replacing that with a default `0` value. But I would recommend against that strongly. – Milad Naseri Jan 14 '12 at 22:00
  • Thanks, your type adaptor comment gave me some ideas. – Soundlink Jan 15 '12 at 00:00
  • Only source of error? You might want to think about that one for a second. – Mike Baranczak Jan 15 '12 at 01:23
  • Definitely. The other property is a String in the JsonPrimitive hierarchy, so it CAN'T cause a NumberFormatException. – Milad Naseri Jan 15 '12 at 01:25
0

I like NO ADAPTERS

forgive me if that looks offensive, but, i had to change my Model Class types all to String to overcome this issue

for example i had

data class Info(
    @SerializedName("name") val name : String?,
    @SerializedName("cover") val cover : String?,
    @SerializedName("releaseDate") val releaseDate : Int?,
    @SerializedName("last_modified") val last_modified : Int?,
    @SerializedName("rating") val rating : Int?)

i was facing NumberFormatException so i changed it to

data class Info(
    @SerializedName("name") val name : String?,
    @SerializedName("cover") val cover : String?,
    @SerializedName("releaseDate") val releaseDate : String?,
    @SerializedName("last_modified") val last_modified : String?,
    @SerializedName("rating") val rating : String?)

Now, i check them as

if(!TextUtils.isEmpty(releaseDate){
//go ahead to use it
}
Nasib
  • 1,173
  • 1
  • 13
  • 23