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!
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;
}
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();
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 TypeToken
s and Type
s 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With