Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON query filter transport

I'd like to transfer from client to server WHERE clause as JSON. I have 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;
   }

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 server and build query.

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

Unfortunately, Date and Integer values deserialize as String and Double.

I have seen examples with TypeToken, custom serializers/deserializers but cannot guess how to apply them to me.

I would happy if you figure out my mistakes, and suggest good idea. Thank you!

like image 752
Azamat Almukhametov Avatar asked Oct 29 '22 10:10

Azamat Almukhametov


1 Answers

Unfortunately, Date and Integer values deserialize as String and Double.

When you define a generic type field like Field<T> -- Gson cannot have enough information on how a certain value should be deserialized to a certain type. It's a fundamental limitation: there is just no type information. Therefore Gson resolves <T> as if it were parameterized as <Object>. When a certain target "slot" (list element, object field, etc) is considered java.lang.Object, Gson resolves a JSON value according to the type of the value literal: if it's something like "...", then it's probably a String; if it's something like 0, then it's definitely a Number and more accurate: Double (doubles are the biggest standard numeric values -- Gson just saves time on numbers type detection and parsing + the user code should have had java.util.List<Number> and detect a particular list element with instanceof -- it might be an integer, a long or a double value -- not very conventient to use, so java.lang.Double is the default strategy). So that you have strings and doubles instead of dates and integers: Gson simple cannot have your desired type information itself.

Why you cannot use type tokens directly: type tokens are used to specify type parameters for elements of the same type, so you cannot have multiple type tokens to cover different types even for a two-element list (list types tokens define a type for all list elements).

To accomplish what you need you can create a type adapter and a respective type adapter factory to perform some kind of lookup to resolve the concrete type. 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 it can be used? I have tested 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 names strategy

If you can supply type names in your JSON to lookup the filter type, then a sample JSON might be look like:

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

Now the Gson instance could be built like this:

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 enhance your JSON documents (at that's good), then you can just replace the strategy, however resolving types can be more complicated due to several reasons since it strongly depends on the given filter value names (the same name might 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();

Note that TypeTokens and Types can be considered immutable and constant, so they can be put 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 stragies, 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 of class java.util.Date
barInteger>=10 of class java.lang.Integer

like image 165
Lyubomyr Shaydariv Avatar answered Nov 09 '22 11:11

Lyubomyr Shaydariv