1

I'm trying to write a general Gson serializer/deserializer for java.javax.JsonObjects:

public static class JavaxJsonObjConverter implements JsonSerializer<JsonObject>, JsonDeserializer<JsonObject> {

  @Override
  public JsonObject deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
    return JsonUtils.getJsonObjectFromString(json.toString());
  }

  @Override
  public JsonElement serialize(JsonObject src, Type typeOfSrc, JsonSerializationContext context) {
    return new JsonParser().parse(src.toString());
  }

}

When I try to serialize a java.json.JsonObject, I get this error:

Exception in thread "main" java.lang.ClassCastException: org.glassfish.json.JsonStringImpl cannot be cast to javax.json.JsonObject
    at om.headlandstech.utils.gson_utils.GsonUtils$JavaxJsonValueConverter.serialize(>GsonUtils.java:1)
    at com.google.gson.internal.bind.TreeTypeAdapter.write(TreeTypeAdapter.java:81)
    at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
    at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapte>rFactory.java:208)
    at ....
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
piyo
  • 461
  • 1
  • 5
  • 18

1 Answers1

3

It would be much better if you'd post the javax.json.JsonObject instance as well (they way it's built). Because: the closest I can reproduce it with is the following:

final Gson gson = new GsonBuilder()
        .registerTypeAdapter(JsonObject.class, new JavaxJsonObjConverter())
        .create();
final JsonObject src = Json.createObjectBuilder()
        .add("foo", "bar")
        .build();
System.out.println(gson.toJson(src.get("foo"), JsonObject.class));

Exception:

Exception in thread "main" java.lang.ClassCastException: org.glassfish.json.JsonStringImpl cannot be cast to javax.json.JsonObject
    at q43376802.Q43376802$JavaxJsonObjConverter.serialize(Q43376802.java:29)
    at com.google.gson.internal.bind.TreeTypeAdapter.write(TreeTypeAdapter.java:81)
    at com.google.gson.Gson.toJson(Gson.java:669)
    at com.google.gson.Gson.toJson(Gson.java:648)
    at com.google.gson.Gson.toJson(Gson.java:603)
    at q43376802.Q43376802.main(Q43376802.java:26)

Next thing. Your JavaxJsonObjConverter implements a javax.json.JavaObject (de)serializer, but javax.json.JavaObject is not the root of JSON objects in javax.json. The hierarchy root is JsonValue. So your (de)serializer must deal with JsonValue rather than JsonObject.

public static class JavaxJsonValConverter
        implements JsonSerializer<JsonValue> {

    @Override
    public JsonElement serialize(final JsonValue jsonValue, final Type type, final JsonSerializationContext context) {
        return new JsonParser().parse(jsonValue.toString());
    }

}

And register it with removing and deleting JavaxJsonObjConverter entirely:

.registerTypeAdapter(JsonValue.class, new JavaxJsonValConverter())

However, the serializer above is naive and requires more resources however, giving you some flexibility (when reading/writing directly from/to JSON streams may be too unjustified (compare DOM and SAX in XML -- it's the same story)):

  • JsonSerializer and JsonDeserializer rely on JSON tree model representation that's implemented with JsonElement. This means that entire JSON has to be loaded into memory and its tree model has to be built before you can use it. This would consume much more memory if JSON objects you're going to deal with are large.
  • toString() is a bad choice either: it requires internal strings to be generated first, thus consuming memory again.

So, the items above may make a really large memory print. In order to save memory resources, you can create a Gson TypeAdapter that can work with JSON streams (and that is the base for every (de)serializer in JSON).

final class JsonValueTypeAdapter
        extends TypeAdapter<JsonValue> {

    private static final TypeAdapter<JsonValue> jsonValueTypeAdapter = new JsonValueTypeAdapter();

    private JsonValueTypeAdapter() {
    }

    static TypeAdapter<JsonValue> getJsonValueTypeAdapter() {
        return jsonValueTypeAdapter;
    }

    @Override
    public void write(final JsonWriter out, final JsonValue jsonValue)
            throws IOException {
        final ValueType valueType = jsonValue.getValueType();
        switch ( valueType ) {
        case ARRAY:
            JsonArrayTypeAdapter.instance.write(out, (JsonArray) jsonValue);
            break;
        case OBJECT:
            JsonObjectTypeAdapter.instance.write(out, (JsonObject) jsonValue);
            break;
        case STRING:
            JsonStringTypeAdapter.instance.write(out, (JsonString) jsonValue);
            break;
        case NUMBER:
            JsonNumberTypeAdapter.instance.write(out, (JsonNumber) jsonValue);
            break;
        case TRUE:
            JsonBooleanTypeAdapter.instance.write(out, jsonValue);
            break;
        case FALSE:
            JsonBooleanTypeAdapter.instance.write(out, jsonValue);
            break;
        case NULL:
            JsonNullTypeAdapter.instance.write(out, jsonValue);
            break;
        default:
            throw new AssertionError(valueType);
        }
    }

    @Override
    public JsonValue read(final JsonReader in)
            throws IOException {
        final JsonToken jsonToken = in.peek();
        switch ( jsonToken ) {
        case BEGIN_ARRAY:
            return JsonArrayTypeAdapter.instance.read(in);
        case END_ARRAY:
            throw new AssertionError("Must never happen due to delegation to the array type adapter");
        case BEGIN_OBJECT:
            return JsonObjectTypeAdapter.instance.read(in);
        case END_OBJECT:
            throw new AssertionError("Must never happen due to delegation to the object type adapter");
        case NAME:
            throw new AssertionError("Must never happen");
        case STRING:
            return JsonStringTypeAdapter.instance.read(in);
        case NUMBER:
            return JsonNumberTypeAdapter.instance.read(in);
        case BOOLEAN:
            return JsonBooleanTypeAdapter.instance.read(in);
        case NULL:
            return JsonNullTypeAdapter.instance.read(in);
        case END_DOCUMENT:
            throw new AssertionError("Must never happen");
        default:
            throw new AssertionError(jsonToken);
        }
    }

    private static final class JsonNullTypeAdapter
            extends TypeAdapter<JsonValue> {

        private static final TypeAdapter<JsonValue> instance = new JsonNullTypeAdapter().nullSafe();

        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter out, final JsonValue jsonNull)
                throws IOException {
            out.nullValue();
        }

        @Override
        public JsonValue read(final JsonReader in)
                throws IOException {
            in.nextNull();
            return JsonValue.NULL;
        }

    }

    private static final class JsonBooleanTypeAdapter
            extends TypeAdapter<JsonValue> {

        private static final TypeAdapter<JsonValue> instance = new JsonBooleanTypeAdapter().nullSafe();

        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter out, final JsonValue jsonBoolean)
                throws IllegalArgumentException, IOException {
            final ValueType valueType = jsonBoolean.getValueType();
            switch ( valueType ) {
            case TRUE:
                out.value(true);
                break;
            case FALSE:
                out.value(false);
                break;
            case ARRAY:
            case OBJECT:
            case STRING:
            case NUMBER:
            case NULL:
                throw new IllegalArgumentException("Not a boolean: " + valueType);
            default:
                throw new AssertionError(jsonBoolean.getValueType());
            }
        }

        @Override
        public JsonValue read(final JsonReader in)
                throws IOException {
            return in.nextBoolean() ? JsonValue.TRUE : JsonValue.FALSE;
        }

    }

    private static final class JsonNumberTypeAdapter
            extends TypeAdapter<JsonNumber> {

        private static final TypeAdapter<JsonNumber> instance = new JsonNumberTypeAdapter().nullSafe();

        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter out, final JsonNumber jsonNumber)
                throws IOException {
            if ( jsonNumber.isIntegral() ) {
                out.value(jsonNumber.longValue());
            } else {
                out.value(jsonNumber.doubleValue());
            }
        }

        @Override
        public JsonNumber read(final JsonReader in)
                throws IOException {
            // TODO is there a good way to instantiate a JsonNumber instance?
            return (JsonNumber) Json.createArrayBuilder()
                    .add(in.nextDouble())
                    .build()
                    .get(0);
        }

    }

    private static final class JsonStringTypeAdapter
            extends TypeAdapter<JsonString> {

        private static final TypeAdapter<JsonString> instance = new JsonStringTypeAdapter().nullSafe();

        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter out, final JsonString jsonString)
                throws IOException {
            out.value(jsonString.getString());
        }

        @Override
        public JsonString read(final JsonReader in)
                throws IOException {
            // TODO is there a good way to instantiate a JsonString instance?
            return (JsonString) Json.createArrayBuilder()
                    .add(in.nextString())
                    .build()
                    .get(0);
        }

    }

    private static final class JsonObjectTypeAdapter
            extends TypeAdapter<JsonObject> {

        private static final TypeAdapter<JsonObject> instance = new JsonObjectTypeAdapter().nullSafe();

        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter out, final JsonObject jsonObject)
                throws IOException {
            out.beginObject();
            for ( final Entry<String, JsonValue> e : jsonObject.entrySet() ) {
                out.name(e.getKey());
                jsonValueTypeAdapter.write(out, e.getValue());
            }
            out.endObject();
        }

        @Override
        public JsonObject read(final JsonReader in)
                throws IOException {
            final JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
            in.beginObject();
            while ( in.hasNext() ) {
                final String key = in.nextName();
                final JsonToken token = in.peek();
                switch ( token ) {
                case BEGIN_ARRAY:
                    jsonObjectBuilder.add(key, jsonValueTypeAdapter.read(in));
                    break;
                case END_ARRAY:
                    throw new AssertionError("Must never happen due to delegation to the array type adapter");
                case BEGIN_OBJECT:
                    jsonObjectBuilder.add(key, jsonValueTypeAdapter.read(in));
                    break;
                case END_OBJECT:
                    throw new AssertionError("Must never happen due to delegation to the object type adapter");
                case NAME:
                    throw new AssertionError("Must never happen");
                case STRING:
                    jsonObjectBuilder.add(key, in.nextString());
                    break;
                case NUMBER:
                    jsonObjectBuilder.add(key, in.nextDouble());
                    break;
                case BOOLEAN:
                    jsonObjectBuilder.add(key, in.nextBoolean());
                    break;
                case NULL:
                    in.nextNull();
                    jsonObjectBuilder.addNull(key);
                    break;
                case END_DOCUMENT:
                    // do nothing
                    break;
                default:
                    throw new AssertionError(token);
                }
            }
            in.endObject();
            return jsonObjectBuilder.build();
        }

    }

    private static final class JsonArrayTypeAdapter
            extends TypeAdapter<JsonArray> {

        private static final TypeAdapter<JsonArray> instance = new JsonArrayTypeAdapter().nullSafe();

        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter out, final JsonArray jsonArray)
                throws IOException {
            out.beginArray();
            for ( final JsonValue jsonValue : jsonArray ) {
                jsonValueTypeAdapter.write(out, jsonValue);
            }
            out.endArray();
        }

        @Override
        public JsonArray read(final JsonReader in)
                throws IOException {
            final JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder();
            in.beginArray();
            while ( in.hasNext() ) {
                final JsonToken token = in.peek();
                switch ( token ) {
                case BEGIN_ARRAY:
                    jsonArrayBuilder.add(jsonValueTypeAdapter.read(in));
                    break;
                case END_ARRAY:
                    throw new AssertionError("Must never happen due to delegation to the array type adapter");
                case BEGIN_OBJECT:
                    jsonArrayBuilder.add(jsonValueTypeAdapter.read(in));
                    break;
                case END_OBJECT:
                    throw new AssertionError("Must never happen due to delegation to the object type adapter");
                case NAME:
                    throw new AssertionError("Must never happen");
                case STRING:
                    jsonArrayBuilder.add(in.nextString());
                    break;
                case NUMBER:
                    jsonArrayBuilder.add(in.nextDouble());
                    break;
                case BOOLEAN:
                    jsonArrayBuilder.add(in.nextBoolean());
                    break;
                case NULL:
                    in.nextNull();
                    jsonArrayBuilder.addNull();
                    break;
                case END_DOCUMENT:
                    // do nothing
                    break;
                default:
                    throw new AssertionError(token);
                }
            }
            in.endArray();
            return jsonArrayBuilder.build();
        }

    }

}

The code above is itself-document I think, despite it's relatively large. Example use:

private static final Gson gson = new GsonBuilder()
        .serializeNulls()
        .registerTypeHierarchyAdapter(JsonValue.class, getJsonValueTypeAdapter())
        .create();

public static void main(final String... args) {
    final JsonValue before = createObjectBuilder()
            .add("boolean", true)
            .add("integer", 3)
            .add("string", "foo")
            .addNull("null")
            .add("array", createArrayBuilder()
                    .add(false)
                    .add(2)
                    .add("bar")
                    .addNull()
                    .build())
            .build();
    System.out.println("before.toString()   = " + before);
    final String json = gson.toJson(before);
    System.out.println("type adapter result = " + json);
    final JsonValue after = gson.fromJson(json, JsonValue.class);
    System.out.println("after.toString()    = " + after);
}

Output:

before.toString()   = {"boolean":true,"integer":3,"string":"foo","null":null,"array":[false,2,"bar",null]}
type adapter result = {"boolean":true,"integer":3,"string":"foo","null":null,"array":[false,2,"bar",null]}
after.toString()    = {"boolean":true,"integer":3.0,"string":"foo","null":null,"array":[false,2.0,"bar",null]}

Note that the integer property value has been changed: 3 is now 3.0. This happens because JSON does not distinguish between integers, longs, floats, doubles, etc: all it can handle is just a number. You cannot really restore the original number: for example, 3 may be both long and double. The most you can do here is not using .nextDouble() in favor of .nextString() and trying to detect which numeric type it can fit the most and constuct a JsonNumber instance respectively (I'm wondering how it can be done in javax.json -- see the TODO comments in the type adapter).

Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
  • Thanks so much. This was a tremendous help. I definitely understand how this stuff works better now. One question, though: why are the booleans getting serialized to null? Is there an easy way to prevent that? They don't even make it to the type adapter functions. They're getting serialized within the gson code as null. – piyo Apr 13 '17 at 13:39
  • @piyo Sorry, I didn't notice it. Give me some time please so I could check it. – Lyubomyr Shaydariv Apr 13 '17 at 13:48
  • @piyo Done: I forgot to add the type adapter with `.registerTypeHierarchyAdapter` so it could manage the whole hierarchy. Now it should work. – Lyubomyr Shaydariv Apr 13 '17 at 13:54
  • Awesome! Thanks so much. It's not a big deal as I can work around it, but I noticed that even with the type hierarchy adapter, if the runtime type of an object is org.glassfish.json.jsonObjectBuilderImpl.JsonObjectImpl, gson won't select the right adapter. Why is that? I'm just trying to understand better how the machinery works. – piyo Apr 13 '17 at 15:30
  • @piyo _even with the type hierarchy adapter, if the runtime type of an object is org.glassfish.json.jsonObjectBuilderImpl.JsonObjectImpl, gson won't select the right adapter_ -- Could you please elaborate? For which cases and what `Gson` object configurations? – Lyubomyr Shaydariv Apr 13 '17 at 15:37
  • Ah, don't worry about it. It's because I'm doing something unusual on my end. I'm wrapping the Gson object in something that that writes the runtime class name into the resulting json so that people can serialize/deserialize things without pass the class as an argument or having to use TypeToken. This normally works great, but in this case, the glassfish class is a private lib class, so it can't cast to it when I deserialize.) Easy corner case to work around, though. Thanks again for all your help! Really great. – piyo Apr 14 '17 at 13:07