package data; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.lang.reflect.Type; /** * JSON deserializer which validates that all fields are present. * * @author Peter Wu. */ public class ValidatingJsonDeserializer implements JsonDeserializer { private final Gson gson; public ValidatingJsonDeserializer() { this(new Gson()); } public ValidatingJsonDeserializer(Gson gson) { this.gson = gson; } @Override public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { T obj; try { obj = gson.fromJson(je, type); } catch (JsonParseException jpe) { DebuggingJsonDeserializer.tryValidate(je.toString(), (Class) type); throw new JsonParseException("Debugger could not find a bug", jpe); } checkObject("", je, obj.getClass()); return obj; } protected final void checkObject(String path, JsonElement je, Class type) throws JsonParseException { JsonObject jsonObj = je.getAsJsonObject(); for (Field f : type.getDeclaredFields()) { JsonElement val = jsonObj.get(f.getName()); if (!jsonObj.has(f.getName()) || val.isJsonNull()) { if (f.getAnnotation(Nullable.class) != null) { // null allowed, skip continue; } throw new JsonParseException("Missing field: " + path + f.getName()); } if (f.getType().equals(String.class)) { if (!val.isJsonPrimitive() || !val.getAsJsonPrimitive().isString()) { throw new JsonParseException("Expected string: " + path + f.getName()); } } tryValidateProperty(path, val, f); } } private void tryValidateProperty(String path, JsonElement je, Field f) throws JsonParseException { Class type = f.getType(); // validates arrays ArrayValidator av = f.getAnnotation(ArrayValidator.class); path += f.getName(); if (av != null) { if (!type.isArray()) { throw new RuntimeException("Invalid " + av.getClass().getName() + " + annotation for " + path); } if (!je.isJsonArray()) { throw new JsonParseException("Expected array: " + path); } JsonArray ja = je.getAsJsonArray(); int minLen = av.minLen(), maxLen = av.maxLen(); if (minLen >= 0 && ja.size() < minLen) { throw new JsonParseException("Array smaller than " + minLen + ": " + path); } if (maxLen >= 0 && ja.size() > maxLen) { throw new JsonParseException("Array larger than " + maxLen + ": " + path); } } // validates objects, recursively Validator v = f.getAnnotation(Validator.class); if (v != null) { if (type.isArray()) { // the class expects an array, so the value must have one too. if (!je.isJsonArray()) { throw new JsonParseException("Expected array: " + path); } JsonArray ja = je.getAsJsonArray(); type = type.getComponentType(); // for each array element, check if the object is valid. for (JsonElement arr_je : ja) { checkObject(path + ".", arr_je, type); } } else { // not an array, assume a verifiable object if (!je.isJsonObject()) { throw new JsonParseException("Expected object: " + path); } checkObject(path + ".", je, type); } } } /** * Marks a member as object that should be validated too. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ArrayValidator { /** * @return Minimal length for array types (-1 if not checked). */ int minLen() default -1; /** * @return Maximum length for array types (-1 if not checked). */ int maxLen() default -1; } /** * Marks a member as object that should be validated too. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Validator { } /** * Marks a member as nullable, that is, it can be missing from the JSON * object. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Nullable { } }