2

Is there a way to configure Gson so that it treats any failed field parse as null instead of throwing a parse exception? Ideally we could catch and log the exception -- but we want the option to keep going with the program even if some fields (or subfields) do not parse as expected.

Example:

Malformed JSON:

{
   "dog": []
}

With classes:

class Farm {
  public Dog dog;
}

class Dog {
  public String name;
}

Gson gson = new Gson();
Farm oldMcdonald = gson.fromJson(json, Farm.class); // should not throw exception
assertNull(oldMcdonald.dog); // should pass
Paul
  • 4,422
  • 5
  • 29
  • 55
  • 1
    isn't it a good thing that it actually throws exception? I would hate it , if it didn't inform me of anything and keep filing my program with nulls....I don't think its a problem, its a feature – nafas Apr 24 '18 at 13:33
  • @nafas It's the difference between fail-safe and fail-fast. Either may be desirable, it depends on the situation. – Michael Apr 24 '18 at 13:35
  • Possible duplicate of [How do I write a custom JSON deserializer for Gson?](https://stackoverflow.com/questions/6096940/how-do-i-write-a-custom-json-deserializer-for-gson) – Michael Apr 24 '18 at 13:36
  • @nafas We have a very large configuration object, of which not all values are of the same importance level for the program to function. We have post-parsing validation which checks the important fields. The configuration object is absolutely necessary for the program to run at all. We recently ran into an issue where a new field was added and the configuration file was updated erroneously to use an array instead of an object. Instead of fixing it in this one place with a custom deserializer we want to find a generic solution. – Paul Apr 24 '18 at 13:47
  • You might think you want to have that possible but what if that error breaks the whole json format? Do you want everything to be null? If that's the case you can just catch the exception and say nothing was read. I think gson validates the string first and then serialize it. For example if you have dog: [, cat:'a cat', properties continue. How would you like it to be serialized? – Veselin Davidov Apr 24 '18 at 13:49
  • Are you at least going to have a valid json every time – Veselin Davidov Apr 24 '18 at 13:49
  • If the problem is only that sometimes it returns an array instead of object (like in your example) then you can check this post http://sachinpatil.com/blog/2012/07/03/gson/ If you have to parse invalid json I think the exception is the way to go – Veselin Davidov Apr 24 '18 at 13:53
  • @VeselinDavidov Yes, it will be valid JSON -- but if it isn't then throwing an exception is fine. – Paul Apr 24 '18 at 13:54
  • @Paul alternatively you can use Jackson instead, I believe it uses getter and setter methods instead of direct access to variables, this way you can create custom setters (e.g. try and catch) – nafas Apr 24 '18 at 14:08

2 Answers2

1

In Gson, it can be implemented pretty easy. Despite the following solution, I guess, seems not to work in any case (for example, primitives), it can be enhanced if necessary.

final class JsonFailSafeTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory instance = new JsonFailSafeTypeAdapterFactory();

    private JsonFailSafeTypeAdapterFactory() {
    }

    static TypeAdapterFactory get() {
        return instance;
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        // We can support non-primitive types only
        if ( typeToken.getRawType().isPrimitive() ) {
            return null;
        }
        final TypeAdapter<T> delegateTypeAdapter = gson.getAdapter(typeToken);
        return new JsonFailSafeTypeAdapter<>(delegateTypeAdapter);
    }

    private static final class JsonFailSafeTypeAdapter<T>
            extends TypeAdapter<T> {

        private final TypeAdapter<T> delegateTypeAdapter;

        private JsonFailSafeTypeAdapter(final TypeAdapter<T> delegateTypeAdapter) {
            this.delegateTypeAdapter = delegateTypeAdapter;
        }

        @Override
        public void write(final JsonWriter out, final T value)
                throws IOException {
            delegateTypeAdapter.write(out, value);
        }

        @Override
        public T read(final JsonReader in)
                throws IOException {
            try {
                return delegateTypeAdapter.read(in);
            } catch ( final MalformedJsonException | RuntimeException ignored ) {
                // Once we get into unexpected JSON token, let's *always* consider a fallback to the default value
                // Well, the default is always `null` anyway, but we'll do more work
                return fallback(in);
            }
        }

        private static <T> T fallback(final JsonReader in)
                throws IOException {
            final JsonToken jsonToken = in.peek();
            switch ( jsonToken ) {
            case BEGIN_ARRAY:
            case BEGIN_OBJECT:
            case NAME:
            case STRING:
            case NUMBER:
            case BOOLEAN:
            case NULL:
                // Assume we're at the beginning of a complex JSON value or a JSON primitive
                in.skipValue();
                break;
            case END_ARRAY:
                // Not sure if we skipValue() can fast-forward this one
                in.endArray();
                break;
            case END_OBJECT:
                // The same
                in.endObject();
                break;
            case END_DOCUMENT:
                // do nothing
                break;
            default:
                throw new AssertionError(jsonToken);
            }
            // Just return null (at least at the moment)
            return null;
        }

    }

}

Now just register the above type factory to handle all types (except java.lang.Object if I'm not mistaken).

private static final Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(JsonFailSafeTypeAdapterFactory.get())
        .create();

public static void main(final String... args)
        throws IOException {
    try ( final JsonReader jsonReader = Resources.getPackageResourceJsonReader(Q50002961.class, "farm.json") ) {
        final Farm oldMcdonald = gson.fromJson(jsonReader, Farm.class);
        if ( oldMcdonald.dog != null ) {
            throw new AssertionError();
        }
        System.out.println(oldMcdonald);
    }
}

Example output:

q50002961.Farm@626b2d4a

Another option is also specifying target fields if there is no need to register the factory globally. For instance:

final class Farm {

    @JsonAdapter(JsonFailSafeTypeAdapterFactory.class)
    final Dog dog = null;

}
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
0

I will post a solution for your problem but it would still require you to change the code on your side. For example if you have configured a property as an object and you receive an array - there is no way to map that properly. So I would suggest to change everything in your code to List and write a custom mapper that creates a list with one element when an object is received. This way you will be flexible to what you receive but you will also need to add some logic to handle problems when you have more than one objects to the array. For your example what would you do if you get 2 dogs? What is the correct behavior?

So I would do it like that:

public class MainClass {

    public static <T> void main(String[] args) throws IOException {
        Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ArrayAdapterFactory()).create();
        // Here I do the opposite - add one dog but expect a collection
        String json = "{ \"dog\": {name=\"Snoopy\"} }"; 
        Farm oldMcdonald = gson.fromJson(json, Farm.class); // should not throw exception
        System.out.println("Dog:"+oldMcdonald.dog.get(0).name); //Works properly
    }
}

class Farm {
    @Expose
    public List<Dog> dog; //All such properties become a list. You handle the situation when there are more than one values
}

class Dog {
    @Expose
    public String name;
}

 class ArrayAdapter<T> extends TypeAdapter<List<T>> {
      private Class<T> adapterclass;

      public ArrayAdapter(Class<T> adapterclass) {
          this.adapterclass = adapterclass;
      }

      public List<T> read(JsonReader reader) throws IOException {

          List<T> list = new ArrayList<T>();

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

          if (reader.peek() == JsonToken.BEGIN_OBJECT) {
              T inning = gson.fromJson(reader, adapterclass);
              list.add(inning);
              // return null; here if you want to return null instead of list with one element

          } else if (reader.peek() == JsonToken.BEGIN_ARRAY) {

              reader.beginArray();
              while (reader.hasNext()) {
                  T inning = gson.fromJson(reader, adapterclass);
                  list.add(inning);
              }
              reader.endArray();

          }

          return list;
      }

      public void write(JsonWriter writer, List<T> value) throws IOException {

      }

    }



 class ArrayAdapterFactory implements TypeAdapterFactory {

  @SuppressWarnings({ "unchecked", "rawtypes" })
  @Override
  public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {

      TypeAdapter<T> typeAdapter = null;

      try {
          if (type.getRawType() == List.class)
              typeAdapter = new ArrayAdapter(
                      (Class) ((ParameterizedType) type.getType())
                              .getActualTypeArguments()[0]);
      } catch (Exception e) {
          e.printStackTrace();
      }

      return typeAdapter;


  }

}

Thanks to http://sachinpatil.com/blog/2012/07/03/gson/ for the idea

Veselin Davidov
  • 7,031
  • 1
  • 15
  • 23