Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected behaviour from Gson

I developed a small application that stores data coming from a device: I chose to store data in JSON format, and the serialization/deserialization of the data works just fine, even if it involves some custom types created by me...but only I work in the IDE (Eclipse, for that matter).

When I export a runnable JAR file though, the deserialization of the data encounters some kind of problem, because the software always throws this exception:

Caused by: java.lang.UnsupportedOperationException: Cannot allocate class LocalDateTime
    at com.google.gson.internal.UnsafeAllocator$4.newInstance(UnsafeAllocator.java:104)
    at com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:225)
    ... 88 common frames omitted

I thought I'd encounter problems with custom types, not a built-in one. At this point, I discovered two things:

  • if I use a full JRE 9 to run the JAR file, the exception is not thrown: I double checked the modules included in the custom JRE I created with Jlink.exe, and everything is included correctly. I still want to use a smaller JRE, so I did not investigate further yet (I guess this explains why in the IDE it works perfectly)
  • I added a custom deserializer to the Gson object (see below), with which I simply manually converted the JSON string into a valid data, and that avoided the exception on the LocalDateTime class...but the exception reappeared simply on another class, this time a custom-made one.

At this point, I guess I can simply add a deserializer for each data type that causes problem, but I'm wondering why the issue won't happen with a full JRE, and why a smaller JRE causes this, even if all the modules required are included. Maybe it's worth mentioning also that I added no custom serializer to the Gson object that saves the data, it is all serialized as per Gson default.

LocalDateTime deserializer:

    @Override
    public LocalDateTime deserialize(JsonElement json, java.lang.reflect.Type type,
                JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {

        JsonObject joDate = json.getAsJsonObject().get("date").getAsJsonObject();
        JsonObject joTime = json.getAsJsonObject().get("time").getAsJsonObject();
        //JSON example: {"date":{"year":2019,"month":1,"day":9},"time":{"hour":6,"minute":14,"second":1,"nano":0}
        return LocalDateTime.of(joDate.get("year").getAsInt(),
                joDate.get("month").getAsInt(),
                joDate.get("day").getAsInt(),
                joTime.get("hour").getAsInt(),
                joTime.get("minute").getAsInt(),
                joTime.get("second").getAsInt(),
                joTime.get("nano").getAsInt());
    }
}

Jdeps.deps modules list:

com.google.gson
java.base
javafx.base
javafx.controls
javafx.fxml
javafx.graphics
org.slf4j

After the answer I received, I opened an issue here.

like image 284
il_boga Avatar asked Oct 20 '25 15:10

il_boga


2 Answers

TL;DR

You need a runtime image (e.g. full JDK or something built with jlink) that includes the module jdk.unsupported.

Full Answer

GSON wants to create instances of classes it deserializes without calling any constructors (so nothing gets initialized without GSON saying so). This can't normally be done, but sun.misc.Unsafe offers a way to do this with the method allocateInstance. To that end, GSON needs an instance of sun.misc.Unsafe. The topmost frame in the call stack is from UnsafeAllocator, which uses common trickery to get Unsafe.

The problem is, sun.misc.Unsafe is in module jdk.unsupported, which is present in a full JDK but you won't usually find in runtime images.

When creating your runtime image with jlink, make sure to include the option --add-modules jdk.unsupported and you should be good to go.

Arguably, GSON should declare an optional dependency on jdk.unsupported with requires static.

like image 110
Nicolai Parlog Avatar answered Oct 23 '25 06:10

Nicolai Parlog


I have faced the same issue when packing compose a desktop application.

update build.gradle file, add an unsupported module.

compose.desktop {
    application {
        mainClass = "MainKt"
        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "admin"
            packageVersion = "1.0.0"
            modules("java.sql")
            modules("jdk.unsupported")
        }
    }
}
like image 22
Vahe Gharibyan Avatar answered Oct 23 '25 07:10

Vahe Gharibyan