Passing JSON Request Filter

I would like to pass the WHERE clause as JSON from client to server. I created FilterInfo.class and Filter.class on the server:

  public class Filter<T> { private String fieldName; private String operand; private T value; } public class FilterInfo { private List<Filter> filters = new ArrayList<Filter>(); private String orderBy; } 

An example of my filterInfo as JSON:

 { "filters": [ { "fieldName" : "Name", "operand" : "=", "value" : "John" }, { "fieldName" : "Age", "operand" : ">=", "value" : "30" } ], "orderBy": "Age" } 

Then it should be great to read this JSON on the server and build the request.

 Gson gson = new GsonBuilder() .setPrettyPrinting() .setDateFormat(Constants.MY_DATE_FORMAT) .create(); FilterInfo filterInfo = gson.fromJson(jsonString, FilterInfo.class); 

Unfortunately, Date and Integer values ​​are deserialized as String and Double .

I have seen examples with TypeToken , custom serializers / deserializers, but can't figure out how to apply them to me.

I would be glad if you find out my mistakes and offer a good idea. Thanks!

+5
source share
1 answer

Unfortunately, Date and Integer values ​​are deserialized as String and Double.

When you define a generic field of type Field<T> , Gson may not have enough information about how a particular value should be deserialized to a specific type. This is a fundamental limitation: no type information. Therefore, Gson resolves <T> as if it were parameterized as <Object> . When a specific target slot (list item, object field, etc.) is considered java.lang.Object , Gson resolves the JSON value according to the type of the literal of the value: if it is something like "..." then it is probably , a String ; if it is something like 0 , then it is definitely a Number and more accurate: Double (doubles the largest standard numeric values ​​- Gson just saves time on finding the type of number and parsing + the user code should have java.util.List<Number> and Define a specific list item using instanceof - it can be an integer, long or double value, which is not very convenient for use, therefore java.lang.Double is the default strategy). So you have strings and doubles instead of dates and integers: Gson simple cannot have the desired type information.

Why you cannot use type tokens directly: type markers are used to indicate type parameters for elements of the same type, so you cannot have several type tokens for different types even for a two-element list (list token types determine the type for all list elements).

To accomplish what you need, you can create a type adapter and an appropriate factory type adapter to do some kind of search for a particular type of solution. Let's say

 final class FilterTypeAdapterFactory implements TypeAdapterFactory { // This is a strategy up to your needs: resolve a java.lang.reflect.Type by a filter object content private final Function<? super JsonObject, ? extends Type> typeResolvingStrategy; private FilterTypeAdapterFactory(final Function<? super JsonObject, ? extends Type> typeResolvingStrategy) { this.typeResolvingStrategy = typeResolvingStrategy; } static TypeAdapterFactory getFilterTypeAdapterFactory(final Function<? super JsonObject, ? extends Type> typeResolvingStrategy) { return new FilterTypeAdapterFactory(typeResolvingStrategy); } @Override public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) { // Is it the Filter class? if ( Filter.class.isAssignableFrom(typeToken.getRawType()) ) { // Get the JsonObject type adapter final TypeAdapter<JsonObject> jsonObjectTypeAdapter = gson.getAdapter(JsonObject.class); // This is a function to resolve a downstream type adapter by the given type // If a downstream parser is not used, then the lookup will end up with self-recursion... final Function<Type, TypeAdapter<T>> typeTypeAdapterFunction = type -> { // Create a type token dynamically @SuppressWarnings("unchecked") final TypeToken<T> delegateTypeToken = (TypeToken<T>) TypeToken.get(type); // And get the downstream type adapter return gson.getDelegateAdapter(this, delegateTypeToken); }; return new FilterTypeAdapter<>(jsonObjectTypeAdapter, typeTypeAdapterFunction, typeResolvingStrategy); } // Not a thing we can handle? Return null, and Gson will try to perform lookup itself return null; } private static final class FilterTypeAdapter<T> extends TypeAdapter<T> { private final TypeAdapter<JsonObject> jsonObjectTypeAdapter; private final Function<? super Type, ? extends TypeAdapter<T>> typeAdapterResolver; private final Function<? super JsonObject, ? extends Type> typeResolvingStrategy; private FilterTypeAdapter( final TypeAdapter<JsonObject> jsonObjectTypeAdapter, final Function<? super Type, ? extends TypeAdapter<T>> typeAdapterResolver, final Function<? super JsonObject, ? extends Type> typeResolvingStrategy ) { this.jsonObjectTypeAdapter = jsonObjectTypeAdapter; this.typeAdapterResolver = typeAdapterResolver; this.typeResolvingStrategy = typeResolvingStrategy; } @Override public void write(final JsonWriter out, final T value) { // If you ever need it, then you have to implement it throw new UnsupportedOperationException(); } @Override public T read(final JsonReader in) throws IOException { // Read the next {...} and convert it to JsonObject final JsonObject jsonObject = jsonObjectTypeAdapter.read(in); // Now resolve a real type by the given JsonObject instance // ... and resolve its type adapter final TypeAdapter<T> delegateTypeAdapter = typeAdapterResolver.apply(typeResolvingStrategy.apply(jsonObject)); // Since the reader has the {...} value already consumed, we cannot read it at this moment // But we can convert the cached JsonObject to the target type object return delegateTypeAdapter.fromJsonTree(jsonObject); } } } 

OK, how can I use it? I checked it with the following mappings:

 final class Filter<T> { final String fieldName = null; final String operand = null; final T value = null; } 
 final class FilterInfo { final List<Filter<?>> filters = null; final String orderBy = null; } 

In-JSON Type Name Strategy

If you can specify type names in your JSON to search for a filter type, then a JSON pattern might look like this:

 { "filters": [ {"_type": "date", "fieldName": "fooDate", "operand": "=", "value": "1997-12-20"}, {"_type": "int", "fieldName": "barInteger", "operand": ">=", "value": 10} ], "orderBy": "fooDate" } 

Now the Gson instance can be created as follows:

 private static final Gson gson = new GsonBuilder() .setDateFormat("yyyy-MM-dd") .registerTypeAdapterFactory(getFilterTypeAdapterFactory(jsonObject -> { if ( !jsonObject.has("_type") ) { return defaultFilterType; } switch ( jsonObject.get("_type").getAsString() ) { case "int": return integerFilterType; case "date": return dateFilterType; default: return defaultFilterType; } })) .create(); 

Alternative strategy

If you don’t want to improve your JSON documents (good at the same time), then you can simply replace the strategy, however, resolving types can be more complicated due to several reasons, since it depends heavily on the given filter value names (the same name can be used for different types):

 { "filters": [ {"fieldName": "fooDate", "operand": "=", "value": "1997-12-20"}, {"fieldName": "barInteger", "operand": ">=", "value": 10} ], "orderBy": "fooDate" } 
 private static final Gson gson = new GsonBuilder() .setDateFormat("yyyy-MM-dd") .registerTypeAdapterFactory(getFilterTypeAdapterFactory(jsonObject -> { if ( !jsonObject.has("fieldName") ) { return defaultFilterType; } switch ( jsonObject.get("fieldName").getAsString() ) { case "barInteger": return integerFilterType; case "fooDate": return dateFilterType; default: return defaultFilterType; } })) .create(); 

Please note that TypeToken and Type can be considered immutable and constant, so they can be placed in a separate class:

 final class Types { private Types() { } static final Type defaultFilterType = new TypeToken<Filter<Object>>() { }.getType(); static final Type integerFilterType = new TypeToken<Filter<Integer>>() { }.getType(); static final Type dateFilterType = new TypeToken<Filter<Date>>() { }.getType(); } 

Now, for both styles, the following code

 final FilterInfo filterInfo = gson.fromJson(JSON, FilterInfo.class); System.out.println(filterInfo.orderBy); for ( final Filter filter : filterInfo.filters ) { System.out.println(filter.fieldName + filter.operand + filter.value + " of " + filter.value.getClass()); } 

will output:

fooDate
fooDate = Sat Dec 20 00:00:00 EET 1997 class java.util.Date
barInteger> = 10 of class java.lang.Integer

+2
source

Source: https://habr.com/ru/post/1265957/


All Articles