From fe0d9e5f890cdb7ac2dba3361335ff55dfdace5c Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 25 May 2026 20:28:01 +0800 Subject: [PATCH 1/5] feat: enhance JsonUtils with nullable type support and additional JSON parsing methods --- .../jackhuang/hmcl/util/gson/JsonUtils.java | 130 +++++++++++++----- 1 file changed, 94 insertions(+), 36 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 703399b12be..06d84981a47 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -22,10 +22,15 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.Reader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -38,6 +43,7 @@ * @author yushijinhun */ @SuppressWarnings("unchecked") +@NotNullByDefault public final class JsonUtils { public static final Gson GSON = defaultGsonBuilder().create(); @@ -51,93 +57,145 @@ public final class JsonUtils { private JsonUtils() { } - public static TypeToken> listTypeOf(Class elementType) { + public static TypeToken> listTypeOf(Class elementType) { return (TypeToken>) TypeToken.getParameterized(List.class, elementType); } - public static TypeToken> listTypeOf(TypeToken elementType) { + public static TypeToken> listTypeOf(TypeToken elementType) { return (TypeToken>) TypeToken.getParameterized(List.class, elementType.getType()); } - public static TypeToken> mapTypeOf(Class keyType, Class valueType) { + public static TypeToken> mapTypeOf(Class keyType, Class valueType) { return (TypeToken>) TypeToken.getParameterized(Map.class, keyType, valueType); } - public static TypeToken> mapTypeOf(Class keyType, TypeToken valueType) { + public static TypeToken> mapTypeOf(Class keyType, TypeToken valueType) { return (TypeToken>) TypeToken.getParameterized(Map.class, keyType, valueType.getType()); } - public static T fromJsonFile(Path file, Class classOfT) throws IOException { - return fromJsonFile(file, TypeToken.get(classOfT)); + public static @Nullable T fromJson(Gson gson, String json, Class type) { + return gson.<@Nullable T>fromJson(json, type); } - public static T fromJsonFile(Path file, TypeToken type) throws IOException { + public static @Nullable T fromJson(String json, Class type) { + return fromJson(GSON, json, type); + } + + public static @Nullable T fromJson(Gson gson, String json, TypeToken type) { + return gson.<@Nullable T>fromJson(json, type); + } + + public static @Nullable T fromJson(String json, TypeToken type) { + return fromJson(GSON, json, type); + } + + public static @Nullable T fromJson(Gson gson, Reader reader, Class type) { + return gson.<@Nullable T>fromJson(reader, type); + } + + public static @Nullable T fromJson(Reader reader, Class type) { + return fromJson(GSON, reader, type); + } + + public static @Nullable T fromJson(Gson gson, Reader reader, TypeToken type) { + return gson.<@Nullable T>fromJson(reader, type); + } + + public static @Nullable T fromJson(Reader reader, TypeToken type) { + return fromJson(GSON, reader, type); + } + + public static @Nullable T fromJsonFile(Gson gson, Path file, Class classOfT) throws IOException { + return fromJsonFile(gson, file, TypeToken.get(classOfT)); + } + + public static @Nullable T fromJsonFile(Path file, Class classOfT) throws IOException { + return fromJsonFile(GSON, file, classOfT); + } + + public static @Nullable T fromJsonFile(Gson gson, Path file, TypeToken type) throws IOException { try (var reader = Files.newBufferedReader(file)) { - return GSON.fromJson(reader, type.getType()); + return gson.fromJson(reader, type.getType()); } } - public static T fromJsonFully(InputStream json, Class classOfT) throws IOException, JsonParseException { + public static @Nullable T fromJsonFile(Path file, TypeToken type) throws IOException { + return fromJsonFile(GSON, file, type); + } + + public static @Nullable T fromJsonFully(InputStream json, Class classOfT) throws IOException, JsonParseException { try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) { - return GSON.fromJson(reader, classOfT); + return GSON.<@Nullable T>fromJson(reader, classOfT); } } - public static T fromJsonFully(InputStream json, TypeToken type) throws IOException, JsonParseException { + public static @Nullable T fromJsonFully(InputStream json, TypeToken type) throws IOException, JsonParseException { try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) { - return GSON.fromJson(reader, type); + return GSON.<@Nullable T>fromJson(reader, type); } } + public static T fromNonNullJson(Gson gson, String json, Class classOfT) throws JsonParseException { + return fromNonNullJson(gson, json, TypeToken.get(classOfT)); + } + public static T fromNonNullJson(String json, Class classOfT) throws JsonParseException { - T parsed = GSON.fromJson(json, classOfT); + return fromNonNullJson(GSON, json, classOfT); + } + + public static T fromNonNullJson(Gson gson, String json, TypeToken type) throws JsonParseException { + T parsed = fromJson(gson, json, type); if (parsed == null) throw new JsonParseException("Json object cannot be null."); return parsed; } public static T fromNonNullJson(String json, TypeToken type) throws JsonParseException { - T parsed = GSON.fromJson(json, type); - if (parsed == null) - throw new JsonParseException("Json object cannot be null."); - return parsed; + return fromNonNullJson(GSON, json, type); } - public static T fromNonNullJsonFully(InputStream json, Class classOfT) throws IOException, JsonParseException { - try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) { - T parsed = GSON.fromJson(reader, classOfT); - if (parsed == null) - throw new JsonParseException("Json object cannot be null."); - return parsed; - } + public static T fromNonNullJsonFully(Gson gson, InputStream inputStream, Class classOfT) throws IOException, JsonParseException { + return fromNonNullJsonFully(gson, inputStream, TypeToken.get(classOfT)); } - public static T fromNonNullJsonFully(InputStream json, TypeToken type) throws IOException, JsonParseException { - try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) { - T parsed = GSON.fromJson(reader, type); + public static T fromNonNullJsonFully(InputStream inputStream, Class classOfT) throws IOException, JsonParseException { + return fromNonNullJsonFully(GSON, inputStream, classOfT); + } + + public static T fromNonNullJsonFully(Gson gson, InputStream inputStream, TypeToken type) throws IOException, JsonParseException { + try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + T parsed = fromJson(gson, reader, type); if (parsed == null) throw new JsonParseException("Json object cannot be null."); return parsed; } } - public static T fromMaybeMalformedJson(String json, Class classOfT) throws JsonParseException { - try { - return GSON.fromJson(json, classOfT); - } catch (JsonSyntaxException e) { - return null; - } + public static T fromNonNullJsonFully(InputStream inputStream, TypeToken type) throws IOException, JsonParseException { + return fromNonNullJsonFully(GSON, inputStream, type); } - public static T fromMaybeMalformedJson(String json, TypeToken type) throws JsonParseException { + public static @Nullable T fromMaybeMalformedJson(Gson gson, String json, Class classOfT) throws JsonParseException { + return fromMaybeMalformedJson(gson, json, TypeToken.get(classOfT)); + } + + public static @Nullable T fromMaybeMalformedJson(String json, Class classOfT) throws JsonParseException { + return fromMaybeMalformedJson(GSON, json, classOfT); + } + + public static @Nullable T fromMaybeMalformedJson(Gson gson, String json, TypeToken type) throws JsonParseException { try { - return GSON.fromJson(json, type); + return gson.fromJson(json, type); } catch (JsonSyntaxException e) { return null; } } - public static void writeToJsonFile(Path file, Object value) throws IOException { + public static @Nullable T fromMaybeMalformedJson(String json, TypeToken type) throws JsonParseException { + return fromMaybeMalformedJson(GSON, json, type); + } + + public static void writeToJsonFile(Path file, @Nullable Object value) throws IOException { try (var writer = Files.newBufferedWriter(file)) { GSON.toJson(value, writer); } From e7e5375a5a49c05911ac5c2ae05c021c4ede5b75 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 25 May 2026 20:34:27 +0800 Subject: [PATCH 2/5] feat: add comprehensive JSON deserialization methods and improve Gson utility functions --- .../jackhuang/hmcl/util/gson/JsonUtils.java | 334 +++++++++++++++++- 1 file changed, 327 insertions(+), 7 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 06d84981a47..37941e99484 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -39,110 +39,319 @@ import java.util.Map; import java.util.UUID; -/** - * @author yushijinhun - */ +/// Utility class providing shared [Gson] instances and convenience methods for +/// JSON serialization and deserialization. +/// +/// Two pre-configured [Gson] instances are exposed: +/// - [GSON] — pretty-printing instance with all standard HMCL type adapters registered. +/// - [UGLY_GSON] — compact (non-pretty) instance with the same adapter set minus +/// complex-map-key serialization and the built-in type adapters for [java.time.Instant], +/// [java.util.UUID], and [java.nio.file.Path]. +/// +/// All `fromJson` / `fromJsonFile` / `fromJsonFully` overloads return `null` when the +/// JSON literal `null` is encountered. The `fromNonNull*` variants throw +/// [JsonParseException] instead of returning `null`. The `fromMaybeMalformed*` variants +/// additionally swallow [com.google.gson.JsonSyntaxException] and return `null` for +/// syntactically invalid input. +/// +/// @author yushijinhun @SuppressWarnings("unchecked") @NotNullByDefault public final class JsonUtils { + /// The default shared [Gson] instance. + /// + /// Configured with: + /// - Pretty printing enabled. + /// - Complex map key serialization enabled. + /// - Type adapters for [java.time.Instant], [java.util.UUID], and [java.nio.file.Path]. + /// - [ValidationTypeAdapterFactory], [LowerCaseEnumTypeAdapterFactory], and + /// [JsonTypeAdapterFactory]. public static final Gson GSON = defaultGsonBuilder().create(); + /// A compact [Gson] instance without pretty printing and without the extra + /// type adapters for [java.time.Instant], [java.util.UUID], and [java.nio.file.Path]. + /// + /// Configured with: + /// - [JsonTypeAdapterFactory] + /// - [ValidationTypeAdapterFactory] + /// - [LowerCaseEnumTypeAdapterFactory] public static final Gson UGLY_GSON = new GsonBuilder() .registerTypeAdapterFactory(JsonTypeAdapterFactory.INSTANCE) .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) .registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE) .create(); + /// Not instantiable. private JsonUtils() { } - public static TypeToken> listTypeOf(Class elementType) { + /// Returns a [TypeToken] representing `List` parameterized with the given element class. + /// + /// @param the element type + /// @param elementType the runtime [Class] of the list element + /// @return a [TypeToken] for `List` + public static TypeToken> listTypeOf(Class elementType) { return (TypeToken>) TypeToken.getParameterized(List.class, elementType); } - public static TypeToken> listTypeOf(TypeToken elementType) { + /// Returns a [TypeToken] representing `List` parameterized with the given element token. + /// + /// @param the element type + /// @param elementType a [TypeToken] describing the element type (may itself be generic) + /// @return a [TypeToken] for `List` + public static TypeToken> listTypeOf(TypeToken elementType) { return (TypeToken>) TypeToken.getParameterized(List.class, elementType.getType()); } - public static TypeToken> mapTypeOf(Class keyType, Class valueType) { + /// Returns a [TypeToken] representing `Map` parameterized with the given key and + /// value classes. + /// + /// @param the key type + /// @param the value type + /// @param keyType the runtime [Class] of the map key + /// @param valueType the runtime [Class] of the map value + /// @return a [TypeToken] for `Map` + public static TypeToken> mapTypeOf(Class keyType, Class valueType) { return (TypeToken>) TypeToken.getParameterized(Map.class, keyType, valueType); } - public static TypeToken> mapTypeOf(Class keyType, TypeToken valueType) { + /// Returns a [TypeToken] representing `Map` parameterized with the given key class + /// and value token. + /// + /// @param the key type + /// @param the value type + /// @param keyType the runtime [Class] of the map key + /// @param valueType a [TypeToken] describing the value type (may itself be generic) + /// @return a [TypeToken] for `Map` + public static TypeToken> mapTypeOf( + Class keyType, TypeToken valueType) { return (TypeToken>) TypeToken.getParameterized(Map.class, keyType, valueType.getType()); } + /// Deserializes the JSON string into an object of the given class using the provided [Gson] + /// instance. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param json the JSON string to parse + /// @param type the target class + /// @return the deserialized object, or `null` if the JSON is `null` + /// @throws com.google.gson.JsonSyntaxException if `json` is not valid JSON for `type` public static @Nullable T fromJson(Gson gson, String json, Class type) { return gson.<@Nullable T>fromJson(json, type); } + /// Deserializes the JSON string into an object of the given class using [GSON]. + /// + /// @param the target type + /// @param json the JSON string to parse + /// @param type the target class + /// @return the deserialized object, or `null` if the JSON is `null` + /// @throws com.google.gson.JsonSyntaxException if `json` is not valid JSON for `type` public static @Nullable T fromJson(String json, Class type) { return fromJson(GSON, json, type); } + /// Deserializes the JSON string into an object described by the given [TypeToken] using the + /// provided [Gson] instance. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param json the JSON string to parse + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON is `null` + /// @throws com.google.gson.JsonSyntaxException if `json` is not valid JSON for `type` public static @Nullable T fromJson(Gson gson, String json, TypeToken type) { return gson.<@Nullable T>fromJson(json, type); } + /// Deserializes the JSON string into an object described by the given [TypeToken] using + /// [GSON]. + /// + /// @param the target type + /// @param json the JSON string to parse + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON is `null` + /// @throws com.google.gson.JsonSyntaxException if `json` is not valid JSON for `type` public static @Nullable T fromJson(String json, TypeToken type) { return fromJson(GSON, json, type); } + /// Deserializes JSON from a [Reader] into an object of the given class using the provided + /// [Gson] instance. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param reader the [Reader] supplying JSON content + /// @param type the target class + /// @return the deserialized object, or `null` if the JSON is `null` + /// @throws com.google.gson.JsonIOException if there is a problem reading from `reader` + /// @throws com.google.gson.JsonSyntaxException if the JSON is malformed public static @Nullable T fromJson(Gson gson, Reader reader, Class type) { return gson.<@Nullable T>fromJson(reader, type); } + /// Deserializes JSON from a [Reader] into an object of the given class using [GSON]. + /// + /// @param the target type + /// @param reader the [Reader] supplying JSON content + /// @param type the target class + /// @return the deserialized object, or `null` if the JSON is `null` + /// @throws com.google.gson.JsonIOException if there is a problem reading from `reader` + /// @throws com.google.gson.JsonSyntaxException if the JSON is malformed public static @Nullable T fromJson(Reader reader, Class type) { return fromJson(GSON, reader, type); } + /// Deserializes JSON from a [Reader] into an object described by the given [TypeToken] + /// using the provided [Gson] instance. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param reader the [Reader] supplying JSON content + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON is `null` + /// @throws com.google.gson.JsonIOException if there is a problem reading from `reader` + /// @throws com.google.gson.JsonSyntaxException if the JSON is malformed public static @Nullable T fromJson(Gson gson, Reader reader, TypeToken type) { return gson.<@Nullable T>fromJson(reader, type); } + /// Deserializes JSON from a [Reader] into an object described by the given [TypeToken] + /// using [GSON]. + /// + /// @param the target type + /// @param reader the [Reader] supplying JSON content + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON is `null` + /// @throws com.google.gson.JsonIOException if there is a problem reading from `reader` + /// @throws com.google.gson.JsonSyntaxException if the JSON is malformed public static @Nullable T fromJson(Reader reader, TypeToken type) { return fromJson(GSON, reader, type); } + /// Reads and deserializes a JSON file at the given [Path] into an object of the given class + /// using the provided [Gson] instance. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param file the path to the JSON file + /// @param classOfT the target class + /// @return the deserialized object, or `null` if the JSON root is `null` + /// @throws IOException if an I/O error occurs reading the file + /// @throws com.google.gson.JsonSyntaxException if the file content is not valid JSON for `classOfT` public static @Nullable T fromJsonFile(Gson gson, Path file, Class classOfT) throws IOException { return fromJsonFile(gson, file, TypeToken.get(classOfT)); } + /// Reads and deserializes a JSON file at the given [Path] into an object of the given class + /// using [GSON]. + /// + /// @param the target type + /// @param file the path to the JSON file + /// @param classOfT the target class + /// @return the deserialized object, or `null` if the JSON root is `null` + /// @throws IOException if an I/O error occurs reading the file + /// @throws com.google.gson.JsonSyntaxException if the file content is not valid JSON for `classOfT` public static @Nullable T fromJsonFile(Path file, Class classOfT) throws IOException { return fromJsonFile(GSON, file, classOfT); } + /// Reads and deserializes a JSON file at the given [Path] into an object described by the + /// given [TypeToken] using the provided [Gson] instance. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param file the path to the JSON file + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON root is `null` + /// @throws IOException if an I/O error occurs reading the file + /// @throws com.google.gson.JsonSyntaxException if the file content is not valid JSON for `type` public static @Nullable T fromJsonFile(Gson gson, Path file, TypeToken type) throws IOException { try (var reader = Files.newBufferedReader(file)) { return gson.fromJson(reader, type.getType()); } } + /// Reads and deserializes a JSON file at the given [Path] into an object described by the + /// given [TypeToken] using [GSON]. + /// + /// @param the target type + /// @param file the path to the JSON file + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON root is `null` + /// @throws IOException if an I/O error occurs reading the file + /// @throws com.google.gson.JsonSyntaxException if the file content is not valid JSON for `type` public static @Nullable T fromJsonFile(Path file, TypeToken type) throws IOException { return fromJsonFile(GSON, file, type); } + /// Reads and deserializes JSON from an [InputStream] (UTF-8) into an object of the given + /// class using [GSON]. The stream is closed after reading. + /// + /// @param the target type + /// @param json the input stream containing UTF-8 JSON + /// @param classOfT the target class + /// @return the deserialized object, or `null` if the JSON root is `null` + /// @throws IOException if an I/O error occurs reading the stream + /// @throws JsonParseException if the JSON is malformed or cannot be deserialized public static @Nullable T fromJsonFully(InputStream json, Class classOfT) throws IOException, JsonParseException { try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) { return GSON.<@Nullable T>fromJson(reader, classOfT); } } + /// Reads and deserializes JSON from an [InputStream] (UTF-8) into an object described by + /// the given [TypeToken] using [GSON]. The stream is closed after reading. + /// + /// @param the target type + /// @param json the input stream containing UTF-8 JSON + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON root is `null` + /// @throws IOException if an I/O error occurs reading the stream + /// @throws JsonParseException if the JSON is malformed or cannot be deserialized public static @Nullable T fromJsonFully(InputStream json, TypeToken type) throws IOException, JsonParseException { try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) { return GSON.<@Nullable T>fromJson(reader, type); } } + /// Deserializes a JSON string into a non-null object of the given class using the provided + /// [Gson] instance. Throws [JsonParseException] if the result would be `null`. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param json the JSON string to parse + /// @param classOfT the target class + /// @return the deserialized object, never `null` + /// @throws JsonParseException if the JSON is `null` or cannot be deserialized public static T fromNonNullJson(Gson gson, String json, Class classOfT) throws JsonParseException { return fromNonNullJson(gson, json, TypeToken.get(classOfT)); } + /// Deserializes a JSON string into a non-null object of the given class using [GSON]. + /// Throws [JsonParseException] if the result would be `null`. + /// + /// @param the target type + /// @param json the JSON string to parse + /// @param classOfT the target class + /// @return the deserialized object, never `null` + /// @throws JsonParseException if the JSON is `null` or cannot be deserialized public static T fromNonNullJson(String json, Class classOfT) throws JsonParseException { return fromNonNullJson(GSON, json, classOfT); } + /// Deserializes a JSON string into a non-null object described by the given [TypeToken] + /// using the provided [Gson] instance. Throws [JsonParseException] if the result would be + /// `null`. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param json the JSON string to parse + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, never `null` + /// @throws JsonParseException if the JSON is `null` or cannot be deserialized public static T fromNonNullJson(Gson gson, String json, TypeToken type) throws JsonParseException { T parsed = fromJson(gson, json, type); if (parsed == null) @@ -150,18 +359,58 @@ public static T fromNonNullJson(Gson gson, String json, TypeToken type) t return parsed; } + /// Deserializes a JSON string into a non-null object described by the given [TypeToken] + /// using [GSON]. Throws [JsonParseException] if the result would be `null`. + /// + /// @param the target type + /// @param json the JSON string to parse + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, never `null` + /// @throws JsonParseException if the JSON is `null` or cannot be deserialized public static T fromNonNullJson(String json, TypeToken type) throws JsonParseException { return fromNonNullJson(GSON, json, type); } + /// Reads and deserializes JSON from an [InputStream] (UTF-8) into a non-null object of the + /// given class using the provided [Gson] instance. The stream is closed after reading. + /// Throws [JsonParseException] if the result would be `null`. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param inputStream the input stream containing UTF-8 JSON + /// @param classOfT the target class + /// @return the deserialized object, never `null` + /// @throws IOException if an I/O error occurs reading the stream + /// @throws JsonParseException if the JSON is `null` or cannot be deserialized public static T fromNonNullJsonFully(Gson gson, InputStream inputStream, Class classOfT) throws IOException, JsonParseException { return fromNonNullJsonFully(gson, inputStream, TypeToken.get(classOfT)); } + /// Reads and deserializes JSON from an [InputStream] (UTF-8) into a non-null object of the + /// given class using [GSON]. The stream is closed after reading. + /// Throws [JsonParseException] if the result would be `null`. + /// + /// @param the target type + /// @param inputStream the input stream containing UTF-8 JSON + /// @param classOfT the target class + /// @return the deserialized object, never `null` + /// @throws IOException if an I/O error occurs reading the stream + /// @throws JsonParseException if the JSON is `null` or cannot be deserialized public static T fromNonNullJsonFully(InputStream inputStream, Class classOfT) throws IOException, JsonParseException { return fromNonNullJsonFully(GSON, inputStream, classOfT); } + /// Reads and deserializes JSON from an [InputStream] (UTF-8) into a non-null object + /// described by the given [TypeToken] using the provided [Gson] instance. The stream is + /// closed after reading. Throws [JsonParseException] if the result would be `null`. + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param inputStream the input stream containing UTF-8 JSON + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, never `null` + /// @throws IOException if an I/O error occurs reading the stream + /// @throws JsonParseException if the JSON is `null` or cannot be deserialized public static T fromNonNullJsonFully(Gson gson, InputStream inputStream, TypeToken type) throws IOException, JsonParseException { try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { T parsed = fromJson(gson, reader, type); @@ -171,18 +420,57 @@ public static T fromNonNullJsonFully(Gson gson, InputStream inputStream, Typ } } + /// Reads and deserializes JSON from an [InputStream] (UTF-8) into a non-null object + /// described by the given [TypeToken] using [GSON]. The stream is closed after reading. + /// Throws [JsonParseException] if the result would be `null`. + /// + /// @param the target type + /// @param inputStream the input stream containing UTF-8 JSON + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, never `null` + /// @throws IOException if an I/O error occurs reading the stream + /// @throws JsonParseException if the JSON is `null` or cannot be deserialized public static T fromNonNullJsonFully(InputStream inputStream, TypeToken type) throws IOException, JsonParseException { return fromNonNullJsonFully(GSON, inputStream, type); } + /// Deserializes a possibly malformed JSON string into an object of the given class using + /// the provided [Gson] instance. Returns `null` if the JSON is syntactically invalid + /// ([com.google.gson.JsonSyntaxException] is swallowed). + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param json the JSON string to parse + /// @param classOfT the target class + /// @return the deserialized object, or `null` if the JSON is `null` or syntactically invalid + /// @throws JsonParseException if the JSON is well-formed but semantically invalid public static @Nullable T fromMaybeMalformedJson(Gson gson, String json, Class classOfT) throws JsonParseException { return fromMaybeMalformedJson(gson, json, TypeToken.get(classOfT)); } + /// Deserializes a possibly malformed JSON string into an object of the given class using + /// [GSON]. Returns `null` if the JSON is syntactically invalid + /// ([com.google.gson.JsonSyntaxException] is swallowed). + /// + /// @param the target type + /// @param json the JSON string to parse + /// @param classOfT the target class + /// @return the deserialized object, or `null` if the JSON is `null` or syntactically invalid + /// @throws JsonParseException if the JSON is well-formed but semantically invalid public static @Nullable T fromMaybeMalformedJson(String json, Class classOfT) throws JsonParseException { return fromMaybeMalformedJson(GSON, json, classOfT); } + /// Deserializes a possibly malformed JSON string into an object described by the given + /// [TypeToken] using the provided [Gson] instance. Returns `null` if the JSON is + /// syntactically invalid ([com.google.gson.JsonSyntaxException] is swallowed). + /// + /// @param the target type + /// @param gson the [Gson] instance to use + /// @param json the JSON string to parse + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON is `null` or syntactically invalid + /// @throws JsonParseException if the JSON is well-formed but semantically invalid public static @Nullable T fromMaybeMalformedJson(Gson gson, String json, TypeToken type) throws JsonParseException { try { return gson.fromJson(json, type); @@ -191,16 +479,48 @@ public static T fromNonNullJsonFully(InputStream inputStream, TypeToken t } } + /// Deserializes a possibly malformed JSON string into an object described by the given + /// [TypeToken] using [GSON]. Returns `null` if the JSON is syntactically invalid + /// ([com.google.gson.JsonSyntaxException] is swallowed). + /// + /// @param the target type + /// @param json the JSON string to parse + /// @param type a [TypeToken] describing the target type + /// @return the deserialized object, or `null` if the JSON is `null` or syntactically invalid + /// @throws JsonParseException if the JSON is well-formed but semantically invalid public static @Nullable T fromMaybeMalformedJson(String json, TypeToken type) throws JsonParseException { return fromMaybeMalformedJson(GSON, json, type); } + /// Serializes `value` to JSON and writes it to the file at the given [Path] using [GSON]. + /// The file is created or overwritten atomically by the underlying [java.nio.file.Files] + /// API. + /// + /// @param file the path to the output file + /// @param value the object to serialize; may be `null`, in which case the JSON `null` + /// literal is written + /// @throws IOException if an I/O error occurs creating or writing the file public static void writeToJsonFile(Path file, @Nullable Object value) throws IOException { try (var writer = Files.newBufferedWriter(file)) { GSON.toJson(value, writer); } } + /// Creates and returns a pre-configured [GsonBuilder] used to construct [GSON]. + /// + /// The builder has the following configuration applied: + /// - Complex map key serialization enabled. + /// - Pretty printing enabled. + /// - [InstantTypeAdapter] registered for [java.time.Instant]. + /// - [UUIDTypeAdapter] registered for [java.util.UUID]. + /// - [PathTypeAdapter] registered for [java.nio.file.Path]. + /// - [ValidationTypeAdapterFactory], [LowerCaseEnumTypeAdapterFactory], and + /// [JsonTypeAdapterFactory] registered as type adapter factories. + /// + /// Callers may further customize the returned builder before calling + /// [GsonBuilder#create()]. + /// + /// @return a new [GsonBuilder] with the default HMCL JSON configuration public static GsonBuilder defaultGsonBuilder() { return new GsonBuilder() .enableComplexMapKeySerialization() From 303f132eb286d1dfe7c4df02089e29307b5005f2 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 25 May 2026 20:39:31 +0800 Subject: [PATCH 3/5] fix: standardize IOException documentation formatting in fromJsonFile methods --- .../java/org/jackhuang/hmcl/util/gson/JsonUtils.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 37941e99484..d6f3db07a60 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -22,7 +22,6 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; @@ -239,7 +238,7 @@ private JsonUtils() { /// @param file the path to the JSON file /// @param classOfT the target class /// @return the deserialized object, or `null` if the JSON root is `null` - /// @throws IOException if an I/O error occurs reading the file + /// @throws IOException if an I/O error occurs reading the file /// @throws com.google.gson.JsonSyntaxException if the file content is not valid JSON for `classOfT` public static @Nullable T fromJsonFile(Gson gson, Path file, Class classOfT) throws IOException { return fromJsonFile(gson, file, TypeToken.get(classOfT)); @@ -252,7 +251,7 @@ private JsonUtils() { /// @param file the path to the JSON file /// @param classOfT the target class /// @return the deserialized object, or `null` if the JSON root is `null` - /// @throws IOException if an I/O error occurs reading the file + /// @throws IOException if an I/O error occurs reading the file /// @throws com.google.gson.JsonSyntaxException if the file content is not valid JSON for `classOfT` public static @Nullable T fromJsonFile(Path file, Class classOfT) throws IOException { return fromJsonFile(GSON, file, classOfT); @@ -266,7 +265,7 @@ private JsonUtils() { /// @param file the path to the JSON file /// @param type a [TypeToken] describing the target type /// @return the deserialized object, or `null` if the JSON root is `null` - /// @throws IOException if an I/O error occurs reading the file + /// @throws IOException if an I/O error occurs reading the file /// @throws com.google.gson.JsonSyntaxException if the file content is not valid JSON for `type` public static @Nullable T fromJsonFile(Gson gson, Path file, TypeToken type) throws IOException { try (var reader = Files.newBufferedReader(file)) { @@ -281,7 +280,7 @@ private JsonUtils() { /// @param file the path to the JSON file /// @param type a [TypeToken] describing the target type /// @return the deserialized object, or `null` if the JSON root is `null` - /// @throws IOException if an I/O error occurs reading the file + /// @throws IOException if an I/O error occurs reading the file /// @throws com.google.gson.JsonSyntaxException if the file content is not valid JSON for `type` public static @Nullable T fromJsonFile(Path file, TypeToken type) throws IOException { return fromJsonFile(GSON, file, type); From 9f9118ca964229f85b0a3bf72a86b87563bd82e1 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 25 May 2026 20:40:14 +0800 Subject: [PATCH 4/5] refactor: remove redundant comments in JsonUtils serialization method --- .../src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index d6f3db07a60..72446b2591d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -492,8 +492,6 @@ public static T fromNonNullJsonFully(InputStream inputStream, TypeToken t } /// Serializes `value` to JSON and writes it to the file at the given [Path] using [GSON]. - /// The file is created or overwritten atomically by the underlying [java.nio.file.Files] - /// API. /// /// @param file the path to the output file /// @param value the object to serialize; may be `null`, in which case the JSON `null` From aa5936f9495199b93c9835b91721387f3c514fd2 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 25 May 2026 20:52:49 +0800 Subject: [PATCH 5/5] refactor: simplify listTypeOf method by removing unnecessary nullable type annotation --- .../src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 72446b2591d..bb130fb885d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -90,7 +90,7 @@ private JsonUtils() { /// @param the element type /// @param elementType the runtime [Class] of the list element /// @return a [TypeToken] for `List` - public static TypeToken> listTypeOf(Class elementType) { + public static TypeToken> listTypeOf(Class elementType) { return (TypeToken>) TypeToken.getParameterized(List.class, elementType); }