Gson optional and required fields

Solution 1:

As you note, Gson has no facility to define a "required field" and you'll just get null in your deserialized object if something is missing in the JSON.

Here's a re-usable deserializer and annotation that will do this. The limitation is that if the POJO required a custom deserializer as-is, you'd have to go a little further and either pass in a Gson object in the constructor to deserialize to object itself or move the annotation checking out into a separate method and use it in your deserializer. You could also improve on the exception handling by creating your own exception and pass it to the JsonParseException so it can be detected via getCause() in the caller.

That all said, in the vast majority of cases, this will work:

public class App
{

    public static void main(String[] args)
    {
        Gson gson =
            new GsonBuilder()
            .registerTypeAdapter(TestAnnotationBean.class, new AnnotatedDeserializer<TestAnnotationBean>())
            .create();

        String json = "{\"foo\":\"This is foo\",\"bar\":\"this is bar\"}";
        TestAnnotationBean tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);

        json = "{\"foo\":\"This is foo\"}";
        tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);

        json = "{\"bar\":\"This is bar\"}";
        tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonRequired
{
}

class TestAnnotationBean
{
    @JsonRequired public String foo;
    public String bar;
}

class AnnotatedDeserializer<T> implements JsonDeserializer<T>
{

    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException
    {
        T pojo = new Gson().fromJson(je, type);

        Field[] fields = pojo.getClass().getDeclaredFields();
        for (Field f : fields)
        {
            if (f.getAnnotation(JsonRequired.class) != null)
            {
                try
                {
                    f.setAccessible(true);
                    if (f.get(pojo) == null)
                    {
                        throw new JsonParseException("Missing field in JSON: " + f.getName());
                    }
                }
                catch (IllegalArgumentException ex)
                {
                    Logger.getLogger(AnnotatedDeserializer.class.getName()).log(Level.SEVERE, null, ex);
                }
                catch (IllegalAccessException ex)
                {
                    Logger.getLogger(AnnotatedDeserializer.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return pojo;

    }
}

Output:

This is foo
this is bar
This is foo
null
Exception in thread "main" com.google.gson.JsonParseException: Missing field in JSON: foo

Solution 2:

Answer of Brian Roach is very good, but sometimes it's also necessary to handle:

  • properties of model's super class
  • properties inside of arrays

For these purposes the following class can be used:

/**
 * Adds the feature to use required fields in models.
 *
 * @param <T> Model to parse to.
 */
public class JsonDeserializerWithOptions<T> implements JsonDeserializer<T> {

    /**
     * To mark required fields of the model:
     * json parsing will be failed if these fields won't be provided.
     * */
    @Retention(RetentionPolicy.RUNTIME) // to make reading of this field possible at the runtime
    @Target(ElementType.FIELD)          // to make annotation accessible through reflection
    public @interface FieldRequired {}

    /**
     * Called when the model is being parsed.
     *
     * @param je   Source json string.
     * @param type Object's model.
     * @param jdc  Unused in this case.
     *
     * @return Parsed object.
     *
     * @throws JsonParseException When parsing is impossible.
     * */
    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
            throws JsonParseException {
        // Parsing object as usual.
        T pojo = new Gson().fromJson(je, type);

        // Getting all fields of the class and checking if all required ones were provided.
        checkRequiredFields(pojo.getClass().getDeclaredFields(), pojo);

        // Checking if all required fields of parent classes were provided.
        checkSuperClasses(pojo);

        // All checks are ok.
        return pojo;
    }

    /**
     * Checks whether all required fields were provided in the class.
     *
     * @param fields Fields to be checked.
     * @param pojo   Instance to check fields in.
     *
     * @throws JsonParseException When some required field was not met.
     * */
    private void checkRequiredFields(@NonNull Field[] fields, @NonNull Object pojo)
            throws JsonParseException {
        // Checking nested list items too.
        if (pojo instanceof List) {
            final List pojoList = (List) pojo;
            for (final Object pojoListPojo : pojoList) {
                checkRequiredFields(pojoListPojo.getClass().getDeclaredFields(), pojoListPojo);
                checkSuperClasses(pojoListPojo);
            }
        }

        for (Field f : fields) {
            // If some field has required annotation.
            if (f.getAnnotation(FieldRequired.class) != null) {
                try {
                    // Trying to read this field's value and check that it truly has value.
                    f.setAccessible(true);
                    Object fieldObject = f.get(pojo);
                    if (fieldObject == null) {
                        // Required value is null - throwing error.
                        throw new JsonParseException(String.format("%1$s -> %2$s",
                                pojo.getClass().getSimpleName(),
                                f.getName()));
                    } else {
                        checkRequiredFields(fieldObject.getClass().getDeclaredFields(), fieldObject);
                        checkSuperClasses(fieldObject);
                    }
                }

                // Exceptions while reflection.
                catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new JsonParseException(e);
                }
            }
        }
    }

    /**
     * Checks whether all super classes have all required fields.
     *
     * @param pojo Object to check required fields in its superclasses.
     *
     * @throws JsonParseException When some required field was not met.
     * */
    private void checkSuperClasses(@NonNull Object pojo) throws JsonParseException {
        Class<?> superclass = pojo.getClass();
        while ((superclass = superclass.getSuperclass()) != null) {
            checkRequiredFields(superclass.getDeclaredFields(), pojo);
        }
    }

}

First of all the interface (annotation) to mark required fields with is described, we'll see an example of its usage later:

    /**
     * To mark required fields of the model:
     * json parsing will be failed if these fields won't be provided.
     * */
    @Retention(RetentionPolicy.RUNTIME) // to make reading of this field possible at the runtime
    @Target(ElementType.FIELD)          // to make annotation accessible throw the reflection
    public @interface FieldRequired {}

Then deserialize method is implemented. It parses json strings as usual: missing properties in result pojo will have null values:

T pojo = new Gson().fromJson(je, type);

Then the recursive check of all fields of the parsed pojo is being launched:

checkRequiredFields(pojo.getClass().getDeclaredFields(), pojo);

Then we also check all fields of pojo's super classes:

checkSuperClasses(pojo);

It's required when some SimpleModel extends its SimpleParentModel and we want to make sure that all properties of SimpleModel marked as required are provided as SimpleParentModel's ones.

Let's take a look on checkRequiredFields method. First of all it checks if some property is instance of List (json array) - in this case all objects of the list should also be checked to make sure that they have all required fields provided too:

if (pojo instanceof List) {
    final List pojoList = (List) pojo;
    for (final Object pojoListPojo : pojoList) {
        checkRequiredFields(pojoListPojo.getClass().getDeclaredFields(), pojoListPojo);
        checkSuperClasses(pojoListPojo);
    }
}

Then we are iterating through all fields of pojo, checking if all fields with FieldRequired annotation are provided (what means these fields are not null). If we have encountered some null property which is required - an exception will be fired. Otherwise another recursive step of the validation will be launched for current field, and properties of parent classes of the field will be checked too:

        for (Field f : fields) {
            // If some field has required annotation.
            if (f.getAnnotation(FieldRequired.class) != null) {
                try {
                    // Trying to read this field's value and check that it truly has value.
                    f.setAccessible(true);
                    Object fieldObject = f.get(pojo);
                    if (fieldObject == null) {
                        // Required value is null - throwing error.
                        throw new JsonParseException(String.format("%1$s -> %2$s",
                                pojo.getClass().getSimpleName(),
                                f.getName()));
                    } else {
                        checkRequiredFields(fieldObject.getClass().getDeclaredFields(), fieldObject);
                        checkSuperClasses(fieldObject);
                    }
                }

                // Exceptions while reflection.
                catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new JsonParseException(e);
                }
            }
        }

And the last method should be reviewed is checkSuperClasses: it just runs the similar required fields validation checking properties of pojo's super classes:

    Class<?> superclass = pojo.getClass();
    while ((superclass = superclass.getSuperclass()) != null) {
        checkRequiredFields(superclass.getDeclaredFields(), pojo);
    }

And finally lets review some example of this JsonDeserializerWithOptions's usage. Assume we have the following models:

private class SimpleModel extends SimpleParentModel {

    @JsonDeserializerWithOptions.FieldRequired Long id;
    @JsonDeserializerWithOptions.FieldRequired NestedModel nested;
    @JsonDeserializerWithOptions.FieldRequired ArrayList<ListModel> list;

}

private class SimpleParentModel {

    @JsonDeserializerWithOptions.FieldRequired Integer rev;

}

private class NestedModel extends NestedParentModel {

    @JsonDeserializerWithOptions.FieldRequired Long id;

}

private class NestedParentModel {

    @JsonDeserializerWithOptions.FieldRequired Integer rev;

}

private class ListModel {

    @JsonDeserializerWithOptions.FieldRequired Long id;

}

We can be sure that SimpleModel will be parsed correctly without exceptions in this way:

final Gson gson = new GsonBuilder()
    .registerTypeAdapter(SimpleModel.class, new JsonDeserializerWithOptions<SimpleModel>())
    .create();

gson.fromJson("{\"list\":[ { \"id\":1 } ], \"id\":1, \"rev\":22, \"nested\": { \"id\":2, \"rev\":2 }}", SimpleModel.class);

Of course, provided solution can be improved and accept more features: for example - validations for nested objects which are not marked with FieldRequired annotation. Currently it's out of answer's scope, but can be added later.