Question

I am trying to serialize Guava Range objects to JSON using Gson, however the default serialization fails, and I'm unsure how to correctly implement a TypeAdapter for this generic type.

Gson gson = new Gson();
Range<Integer> range = Range.closed(10, 20);
String json = gson.toJson(range);
System.out.println(json);
Range<Integer> range2 = gson.fromJson(json, 
                            new TypeToken<Range<Integer>>(){}.getType());
System.out.println(range2);
assertEquals(range2, range);

This fails like so:

{"lowerBound":{"endpoint":10},"upperBound":{"endpoint":20}}
PASSED: typeTokenInterface
FAILED: range
java.lang.RuntimeException: Unable to invoke no-args constructor for
        com.google.common.collect.Cut<java.lang.Integer>. Register an
        InstanceCreator with Gson for this type may fix this problem.
    at com.google.gson.internal.ConstructorConstructor$12.construct(
        ConstructorConstructor.java:210)
    ...

Note that the default serialization actually loses information - it fails to report whether the endpoints are open or closed. I would prefer to see it serialized similar to its toString(), e.g. [10‥20] however simply calling toString() won't work with generic Range instances, as the elements of the range may not be primitives (Joda-Time LocalDate instances, for example). For the same reason, implementing a custom TypeAdapter seems difficult, as we don't know how to deserialize the endpoints.

I've implemented most of a TypeAdaptorFactory based on the template provided for Multimap which ought to work, but now I'm stuck on the generics. Here's what I have so far:

public class RangeTypeAdapterFactory implements TypeAdapterFactory {
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
    Type type = typeToken.getType();
    if (typeToken.getRawType() != Range.class
        || !(type instanceof ParameterizedType)) {
      return null;
    }

    Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
    TypeAdapter<?> elementAdapter = (TypeAdapter<?>)gson.getAdapter(TypeToken.get(elementType));
    // Bound mismatch: The generic method newRangeAdapter(TypeAdapter<E>) of type
    // GsonUtils.RangeTypeAdapterFactory is not applicable for the arguments
    // (TypeAdapter<capture#4-of ?>). The inferred type capture#4-of ? is not a valid
    // substitute for the bounded parameter <E extends Comparable<?>>
    return (TypeAdapter<T>) newRangeAdapter(elementAdapter);
  }

  private <E extends Comparable<?>> TypeAdapter<Range<E>> newRangeAdapter(final TypeAdapter<E> elementAdapter) {
    return new TypeAdapter<Range<E>>() {
      @Override
      public void write(JsonWriter out, Range<E> value) throws IOException {
        if (value == null) {
          out.nullValue();
          return;
        }

        String repr = (value.lowerBoundType() == BoundType.CLOSED ? "[" : "(") +
                      (value.hasLowerBound() ? elementAdapter.toJson(value.lowerEndpoint()) : "-\u221e") +
                      '\u2025' +
                      (value.hasLowerBound() ? elementAdapter.toJson(value.upperEndpoint()) : "+\u221e") +
                      (value.upperBoundType() == BoundType.CLOSED ? "]" : ")");
        out.value(repr);
      }

      public Range<E> read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
          in.nextNull();
          return null;
        }

        String[] endpoints = in.nextString().split("\u2025");
        E lower = elementAdapter.fromJson(endpoints[0].substring(1));
        E upper = elementAdapter.fromJson(endpoints[1].substring(0,endpoints[1].length()-1));

        return Range.range(lower, endpoints[0].charAt(0) == '[' ? BoundType.CLOSED : BoundType.OPEN,
                           upper, endpoints[1].charAt(endpoints[1].length()-1) == '[' ? BoundType.CLOSED : BoundType.OPEN);
      }
    };
  }
}

However the return (TypeAdapter<T>) newRangeAdapter(elementAdapter); line has a compilation error and I'm now at a loss.

What's the best way to resolve this error? Is there a better way to serialize Range objects that I'm missing? What about if I want to serialize RangeSets?

Rather frustrating that the Google utility library and Google serialization library seem to require so much glue to work together :(

Was it helpful?

Solution

This feels somewhat like reinventing the wheel, but it was a lot quicker to put together and test than the time spent trying to get Gson to behave, so at least presently I'll be using the following Converters to serialize Range and RangeSet*, rather than Gson.

/**
 * Converter between Range instances and Strings, essentially a custom serializer.
 * Ideally we'd let Gson or Guava do this for us, but presently this is cleaner.
 */
public static <T extends Comparable<? super T>> Converter<Range<T>, String> rangeConverter(final Converter<T, String> elementConverter) {
  final String NEG_INFINITY = "-\u221e";
  final String POS_INFINITY = "+\u221e";
  final String DOTDOT = "\u2025";
  return new Converter<Range<T>, String>() {
    @Override
    protected String doForward(Range<T> range) {
      return (range.hasLowerBound() && range.lowerBoundType() == BoundType.CLOSED ? "[" : "(") +
             (range.hasLowerBound() ? elementConverter.convert(range.lowerEndpoint()) : NEG_INFINITY) +
             DOTDOT +
             (range.hasUpperBound() ? elementConverter.convert(range.upperEndpoint()) : POS_INFINITY) +
             (range.hasUpperBound() && range.upperBoundType() == BoundType.CLOSED ? "]" : ")");
    }

    @Override
    protected Range<T> doBackward(String range) {
      String[] endpoints = range.split(DOTDOT);

      Range<T> ret = Range.all();
      if(!endpoints[0].substring(1).equals(NEG_INFINITY)) {
        T lower = elementConverter.reverse().convert(endpoints[0].substring(1));
        ret = ret.intersection(Range.downTo(lower, endpoints[0].charAt(0) == '[' ? BoundType.CLOSED : BoundType.OPEN));
      }
      if(!endpoints[1].substring(0,endpoints[1].length()-1).equals(POS_INFINITY)) {
        T upper = elementConverter.reverse().convert(endpoints[1].substring(0,endpoints[1].length()-1));
        ret = ret.intersection(Range.upTo(upper, endpoints[1].charAt(endpoints[1].length()-1) == ']' ? BoundType.CLOSED : BoundType.OPEN));
      }
      return ret;
    }
  };
}

/**
 * Converter between RangeSet instances and Strings, essentially a custom serializer.
 * Ideally we'd let Gson or Guava do this for us, but presently this is cleaner.
 */
public static <T extends Comparable<? super T>> Converter<RangeSet<T>, String> rangeSetConverter(final Converter<T, String> elementConverter) {
  return new Converter<RangeSet<T>, String>() {
    private final Converter<Range<T>, String> rangeConverter = rangeConverter(elementConverter);
    @Override
    protected String doForward(RangeSet<T> rs) {
      ArrayList<String> ls = new ArrayList<>();
      for(Range<T> range : rs.asRanges()) {
        ls.add(rangeConverter.convert(range));
      }
      return Joiner.on(", ").join(ls);
    }

    @Override
    protected RangeSet<T> doBackward(String rs) {
      Iterable<String> parts = Splitter.on(",").trimResults().split(rs);
      ImmutableRangeSet.Builder<T> build = ImmutableRangeSet.builder();
      for(String range : parts) {
        build.add(rangeConverter.reverse().convert(range));
      }
      return build.build();
    }
  };
}

*For inter-process communication, Java serialization would likely work just fine, as both classes implement Serializable. However I'm serializing to disk for more permanent storage, meaning I need a format I can trust won't change over time. Guava's serialization doesn't provide that guarantee.

OTHER TIPS

Here is a Gson JsonSerializer and JsonDeserializer that generically supports a Range: https://github.com/jamespedwards42/Fava/wiki/Range-Marshaller

@Override
public JsonElement serialize(final Range src, final Type typeOfSrc, final JsonSerializationContext context) {
    final JsonObject jsonObject = new JsonObject();
    if ( src.hasLowerBound() ) {
        jsonObject.add( "lowerBoundType", context.serialize( src.lowerBoundType() ) );
        jsonObject.add( "lowerBound", context.serialize( src.lowerEndpoint() ) );
    } else
        jsonObject.add( "lowerBoundType", context.serialize( BoundType.OPEN ) );

    if ( src.hasUpperBound() ) {
        jsonObject.add( "upperBoundType", context.serialize( src.upperBoundType() ) );
        jsonObject.add( "upperBound", context.serialize( src.upperEndpoint() ) );
    } else
        jsonObject.add( "upperBoundType", context.serialize( BoundType.OPEN ) );
    return jsonObject;
}

@Override
public Range<? extends Comparable<?>> deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
    if ( !( typeOfT instanceof ParameterizedType ) )
        throw new IllegalStateException( "typeOfT must be a parameterized Range." );

    final JsonObject jsonObject = json.getAsJsonObject();
    final JsonElement lowerBoundTypeJsonElement = jsonObject.get( "lowerBoundType" );
    final JsonElement upperBoundTypeJsonElement = jsonObject.get( "upperBoundType" );

    if ( lowerBoundTypeJsonElement == null || upperBoundTypeJsonElement == null )
        throw new IllegalStateException( "Range " + json
                + "was not serialized with this serializer!  The default serialization does not store the boundary types, therfore we can not deserialize." );

    final Type type = ( ( ParameterizedType ) typeOfT ).getActualTypeArguments()[0];

    final BoundType lowerBoundType = context.deserialize( lowerBoundTypeJsonElement, BoundType.class );
    final JsonElement lowerBoundJsonElement = jsonObject.get( "lowerBound" );
    final Comparable<?> lowerBound = lowerBoundJsonElement == null ? null : context.deserialize( lowerBoundJsonElement, type );

    final BoundType upperBoundType = context.deserialize( upperBoundTypeJsonElement, BoundType.class );
    final JsonElement upperBoundJsonElement = jsonObject.get( "upperBound" );
    final Comparable<?> upperBound = upperBoundJsonElement == null ? null : context.deserialize( upperBoundJsonElement, type );

    if ( lowerBound == null && upperBound != null )
        return Range.upTo( upperBound, upperBoundType );
    else if ( lowerBound != null && upperBound == null )
        return Range.downTo( lowerBound, lowerBoundType );
    else if ( lowerBound == null && upperBound == null )
        return Range.all();

    return Range.range( lowerBound, lowerBoundType, upperBound, upperBoundType );
}

Here is a straight forward solution. Works very well

import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import com.google.gson.*;

import java.lang.reflect.Type;

public class GoogleRangeAdapter implements JsonSerializer, JsonDeserializer {
    public static String TK_hasLowerBound = "hasLowerBound";
    public static String TK_hasUpperBound = "hasUpperBound";

    public static String TK_lowerBoundType = "lowerBoundType";
    public static String TK_upperBoundType = "upperBoundType";

    public static String TK_lowerBound = "lowerBound";
    public static String TK_upperBound = "upperBound";

    @Override
    public Object deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        JsonObject jsonObject = (JsonObject)json;
        boolean hasLowerBound = jsonObject.get(TK_hasLowerBound).getAsBoolean();
        boolean hasUpperBound = jsonObject.get(TK_hasUpperBound).getAsBoolean();

        if (!hasLowerBound && !hasUpperBound) {
            return Range.all();
        }
        else if (!hasLowerBound && hasUpperBound){
            double upperBound = jsonObject.get(TK_upperBound).getAsDouble();
            BoundType upperBoundType = BoundType.valueOf(jsonObject.get(TK_upperBoundType).getAsString());
            if (upperBoundType == BoundType.OPEN)
                return Range.lessThan(upperBound);
            else
                return Range.atMost(upperBound);
        }
        else if (hasLowerBound && !hasUpperBound){
            double lowerBound = jsonObject.get(TK_lowerBound).getAsDouble();
            BoundType lowerBoundType = BoundType.valueOf(jsonObject.get(TK_lowerBoundType).getAsString());
            if (lowerBoundType == BoundType.OPEN)
                return Range.greaterThan(lowerBound);
            else
                return Range.atLeast(lowerBound);
        }
        else {
            double lowerBound = jsonObject.get(TK_lowerBound).getAsDouble();
            double upperBound = jsonObject.get(TK_upperBound).getAsDouble();
            BoundType upperBoundType = BoundType.valueOf(jsonObject.get(TK_upperBoundType).getAsString());
            BoundType lowerBoundType = BoundType.valueOf(jsonObject.get(TK_lowerBoundType).getAsString());
            if (lowerBoundType == BoundType.OPEN && upperBoundType == BoundType.OPEN)
                return Range.open(lowerBound, upperBound);
            else if (lowerBoundType == BoundType.OPEN && upperBoundType == BoundType.CLOSED)
                return Range.openClosed(lowerBound, upperBound);
            else if (lowerBoundType == BoundType.CLOSED && upperBoundType == BoundType.OPEN)
                return Range.closedOpen(lowerBound, upperBound);
            else
                return Range.closed(lowerBound, upperBound);
        }
    }

    @Override
    public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject jsonObject = new JsonObject();

        Range<Double> range = (Range<Double>)src;
        boolean hasLowerBound = range.hasLowerBound();
        boolean hasUpperBound = range.hasUpperBound();

        jsonObject.addProperty(TK_hasLowerBound, hasLowerBound);
        jsonObject.addProperty(TK_hasUpperBound, hasUpperBound);

        if (hasLowerBound) {
            jsonObject.addProperty(TK_lowerBound, range.lowerEndpoint());
            jsonObject.addProperty(TK_lowerBoundType, range.lowerBoundType().name());
        }

        if (hasUpperBound) {
            jsonObject.addProperty(TK_upperBound, range.upperEndpoint());
            jsonObject.addProperty(TK_upperBoundType, range.upperBoundType().name());
        }

        return jsonObject;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top