summaryrefslogtreecommitdiff
path: root/src/data/ValidatingJsonDeserializer.java
blob: d5a8dbcb080fe0db60e9bc21283330eb4c1926eb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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<T> implements JsonDeserializer<T> {

    @Override
    public T deserialize(JsonElement je, Type type,
            JsonDeserializationContext jdc) throws JsonParseException {
        T obj;
        try {
            obj = new 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;
    }

    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);
            // TODO: validate type?
        }
    }

    private void tryValidateProperty(String path, JsonElement je, Field f)
            throws JsonParseException {
        Class<?> type = f.getType();
        // assume that this annotation is only applied to objects
        Validator v = f.getAnnotation(Validator.class);
        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);
            }
        }
        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 {
    }
}