Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JDK21 toJson failure #2689

Open
4 tasks
funky-eyes opened this issue May 29, 2024 · 6 comments
Open
4 tasks

JDK21 toJson failure #2689

funky-eyes opened this issue May 29, 2024 · 6 comments
Labels

Comments

@funky-eyes
Copy link

Gson version

version 2.10.1
Using JDK 21 to perform fromJson, the Date type contains hidden characters causing the failure of the toJson operation.

In GitHub issues, it seems that I cannot illustrate it, so I will use images to show
image

    public static void main(String[] args) {
        String jdk17="{\"clientId\":\"123\",\"secret\":\"123\",\"creator\":\"trump\",\"gmtCreated\":\"Dec 14, 2023, 11:07:35 AM\",\"gmtModified\":\"Dec 15, 2023, 4:45:51 PM\"}";
        OauthClient oc17 = gson.fromJson(jdk17, OauthClient.class);
        String jdk21= "{\"clientId\":\"123\",\"secret\":\"123\",\"creator\":\"trump\",\"gmtCreated\":\"Mar 21, 2022, 11:03:07 AM\",\"gmtModified\":\"Mar 21, 2022, 11:03:07 AM\"}";
        OauthClient oc21 = gson.fromJson(jdk21, OauthClient.class);
    }
Exception in thread "main" com.google.gson.JsonSyntaxException: Failed parsing 'Mar 21, 2022, 11:03:07 AM' as Date; at path $.gmtCreated
	at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:90)
	at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:75)
	at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:46)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.readIntoField(ReflectiveTypeAdapterFactory.java:212)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$FieldReflectionAdapter.readField(ReflectiveTypeAdapterFactory.java:433)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:393)
	at com.google.gson.Gson.fromJson(Gson.java:1227)
	at com.google.gson.Gson.fromJson(Gson.java:1137)
	at com.google.gson.Gson.fromJson(Gson.java:1047)
	at com.google.gson.Gson.fromJson(Gson.java:982)
	at cn.tongdun.arch.luc.biz.OauthClientBusinessService.main(OauthClientBusinessService.java:78)
Caused by: java.text.ParseException: Failed to parse date ["Mar 21, 2022, 11:03:07 AM"]: Invalid number: Mar 
	at com.google.gson.internal.bind.util.ISO8601Utils.parse(ISO8601Utils.java:279)
	at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:88)
	... 10 more
Caused by: java.lang.NumberFormatException: Invalid number: Mar 
	at com.google.gson.internal.bind.util.ISO8601Utils.parseInt(ISO8601Utils.java:316)
	at com.google.gson.internal.bind.util.ISO8601Utils.parse(ISO8601Utils.java:133)
	... 11 more

Java / Android version

jdk21

Used tools

  • Maven; version:
  • Gradle; version:
  • ProGuard (attach the configuration file please); version:
  • ...

Description

Expected behavior

Actual behavior

Reproduction steps

1.Convert an object containing a date variable to JSON.
2.Copy the output JSON string.
3.When pasted into an editor like IntelliJ IDEA, hidden characters appear with JDK 21, while JDK 17 does not exhibit this behavior for such operations.

Exception stack trace


@funky-eyes funky-eyes added the bug label May 29, 2024
@funky-eyes
Copy link
Author

@jerboaa
Copy link

jerboaa commented May 29, 2024

The relevant JDK enhancement is JDK-8284840 - CLDR update to version 42.0. See also https://bugs.openjdk.org/browse/JDK-8324308 (and friends), for similar issues.

@Marcono1234
Copy link
Collaborator

Gson unfortunately uses a human-readable date format by default. We noticed the same (or a very similar issue) you are seeing here in Gson's unit tests: #2450

There is the idea to possibly change the default date format used by Gson in future versions, see #2472, but it is unclear if that will happen since it could be considered backward incompatible.

The best solution at the moment might be to specify a custom date format with GsonBuilder.setDateFormat(String) or to register a custom TypeAdapter for Date.
Otherwise, if you keep using Gson's default date format a similar issue might happen in future JDK versions again.

@funky-eyes
Copy link
Author

GsonBuilder.setDateFormat(String)

It seems like this is a destructive way of modification. The application that I have already launched will be affected by setDateFormat. For example, if I toJSON some data into Redis and then fromJson to an object when using it, when some data is written by the node using setDateFormat, an exception will occur when other nodes read it. It is impossible to smoothly upgrade. Of course, for Redis, I can switch to another database to solve the problem. If it is stored in other databases such as MySQL, historical data will be seriously affected!

@Marcono1234
Copy link
Collaborator

Marcono1234 commented May 30, 2024

Another solution could be to write a TypeAdapterFactory which creates an adapter that changes the serialization date format, and for deserialization tries to use the same format as well but if that fails falls back to the default adapter.

Here is a sample implementation for this:

IsoDateAdapterFactory (click to expand)

Note that I haven't tested this extensively. And if parsing fails, the JSON path in the exception message will not be that helpful, saying $ (root element) instead of the actual path. Maybe that could be improved a bit by wrapping the exception and including JsonReader.getPreviousPath().

class IsoDateAdapterFactory implements TypeAdapterFactory {
  @Override
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    if (type.getRawType() != Date.class) {
      return null;
    }

    TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);

    // Type check above made sure adapter is requested for `Date`
    @SuppressWarnings("unchecked")
    TypeAdapter<Date> fallback = (TypeAdapter<Date>) gson.getDelegateAdapter(this, type);
    @SuppressWarnings("unchecked")
    TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<Date>() {
      @Override
      public void write(JsonWriter out, Date value) throws IOException {
        if (value == null) {
          out.nullValue();
          return;
        }

        Instant instant = value.toInstant();
        // Write instant in ISO format
        out.value(instant.toString());
      }

      @Override
      public Date read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
          in.nextNull();
          return null;
        }

        // First read as JsonElement tree to be able to parse it twice (once with ISO format,
        // and otherwise with default `Date` adapter as fallback)
        JsonElement json = jsonElementAdapter.read(in);
        try {
          String dateString = json.getAsJsonPrimitive().getAsString();
          // Parse with ISO format
          Instant instant = Instant.parse(dateString);
          return Date.from(instant);
        } catch (Exception e) {
          try {
            return fallback.fromJsonTree(json);
          } catch (Exception suppressed) {
            e.addSuppressed(suppressed);
            throw e;
          }
        }
      }
    };
    return adapter;
  }
}

And then registering it like this:

Gson gson = new GsonBuilder()
  .registerTypeAdapterFactory(new IsoDateAdapterFactory())
  .create();

@funky-eyes
Copy link
Author

Another solution could be to write a TypeAdapterFactory which creates an adapter that changes the serialization date format, and for deserialization tries to use the same format as well but if that fails falls back to the default adapter.

Here is a sample implementation for this:

IsoDateAdapterFactory (click to expand)
Note that I haven't tested this extensively. And if parsing fails, the JSON path in the exception message will not be that helpful, saying $ (root element) instead of the actual path. Maybe that could be improved a bit by wrapping the exception and including JsonReader.getPreviousPath().

class IsoDateAdapterFactory implements TypeAdapterFactory {
  @Override
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    if (type.getRawType() != Date.class) {
      return null;
    }

    TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);

    // Type check above made sure adapter is requested for `Date`
    @SuppressWarnings("unchecked")
    TypeAdapter<Date> fallback = (TypeAdapter<Date>) gson.getDelegateAdapter(this, type);
    @SuppressWarnings("unchecked")
    TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<Date>() {
      @Override
      public void write(JsonWriter out, Date value) throws IOException {
        if (value == null) {
          out.nullValue();
          return;
        }

        Instant instant = value.toInstant();
        // Write instant in ISO format
        out.value(instant.toString());
      }

      @Override
      public Date read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
          in.nextNull();
          return null;
        }

        // First read as JsonElement tree to be able to parse it twice (once with ISO format,
        // and otherwise with default `Date` adapter as fallback)
        JsonElement json = jsonElementAdapter.read(in);
        try {
          String dateString = json.getAsJsonPrimitive().getAsString();
          // Parse with ISO format
          Instant instant = Instant.parse(dateString);
          return Date.from(instant);
        } catch (Exception e) {
          try {
            return fallback.fromJsonTree(json);
          } catch (Exception suppressed) {
            e.addSuppressed(suppressed);
            throw e;
          }
        }
      }
    };
    return adapter;
  }
}

And then registering it like this:

Gson gson = new GsonBuilder()
  .registerTypeAdapterFactory(new IsoDateAdapterFactory())
  .create();

Thank you for your guidance. I will proceed with the relevant attempts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants